ποΈGitΠ―ΡΠ°ποΈ
Commit f4b6b02acecdd8855408db7d92757f2495fbc11f
Parents : d24fc9a
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-05-18T07:44:12-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-05-18T12:44:12Z
refactor(build): rename entry modules and remove DESKTOP_ONLY mode (#5476)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Changes
356 files changed, 17720 insertions(+), 16805 deletions(-)
Diff
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 583acbae86..d357d0d989 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -18,7 +18,7 @@ git submodule update --init
./gradlew :core:data:allTests
# Single module tests (Android-only module like :app)
-./gradlew :app:testFdroidDebugUnitTest
+./gradlew :androidApp:testFdroidDebugUnitTest
# Cross-platform compilation check (no tests)
./gradlew kmpSmokeCompile
@@ -46,7 +46,7 @@ KMP modules have different task names than pure-Android modules. Using the wrong
## Quick Reference
-- **Architecture**: KMP project (Android, Desktop, iOS). Business logic in `commonMain`; platform shells (`app/`, `desktop/`) wire DI and host UI. See `AGENTS.md` and `.skills/kmp-architecture/`.
+- **Architecture**: KMP project (Android, Desktop, iOS). Business logic in `commonMain`; platform shells (`androidApp/`, `desktopApp/`) wire DI and host UI. See `AGENTS.md` and `.skills/kmp-architecture/`.
- **Flavors**: `fdroid` (OSS) / `google` (Maps + DataDog). Only one installable at a time (different signing keys).
- **Verify before push**: Run `./gradlew spotlessApply detekt assembleDebug test allTests`, then confirm CI with `gh pr checks <PR>`.
- **Strings**: `stringResource(Res.string.key)` β run `python3 scripts/sort-strings.py` after adding strings.
diff --git a/.github/instructions/ci-workflows.instructions.md b/.github/instructions/ci-workflows.instructions.md
index 55a72b3288..fceb3270e0 100644
--- a/.github/instructions/ci-workflows.instructions.md
+++ b/.github/instructions/ci-workflows.instructions.md
@@ -5,7 +5,7 @@ excludeAgent: "code-review"
# CI Workflow Rules
-- Prefer explicit Gradle task paths (`app:lintFdroidDebug`) over shorthand (`lintDebug`).
+- Prefer explicit Gradle task paths (`androidApp:lintFdroidDebug`) over shorthand (`lintDebug`).
- CI uses `.github/ci-gradle.properties` β don't assume local `gradle.properties` values.
- CI passes `-Pci=true` to enable full processor usage via `maxParallelForks`.
- Use `fetch-depth: 0` only where needed (spotless ratcheting, version code). Use `fetch-depth: 1` otherwise.
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index f7c8151c72..ed21cdd606 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -8,10 +8,10 @@ on:
- main
paths:
# Only rebuild docs when source code changes (Dokka generates from KDoc)
- - 'app/src/**'
+ - 'androidApp/src/**'
- 'core/**/src/**'
- 'feature/**/src/**'
- - 'desktop/src/**'
+ - 'desktopApp/src/**'
- 'build-logic/**'
- 'build.gradle.kts'
- 'settings.gradle.kts'
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 0e70c3c410..fcb82906c6 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -30,9 +30,9 @@ jobs:
- '.github/workflows/**'
- '.github/actions/**'
# Product modules validated by reusable-check
- - 'app/**'
+ - 'androidApp/**'
- 'baselineprofile/**'
- - 'desktop/**'
+ - 'desktopApp/**'
- 'core/**'
- 'feature/**'
- 'screenshot-tests/**'
@@ -95,9 +95,9 @@ jobs:
PY
# 2. VALIDATION & BUILD: Delegate to reusable-check.yml
- # We disable coverage for PRs to keep feedback fast (< 10 mins).
- # Desktop compilation is covered by :desktop:test in the shard-app test shard.
- # Native desktop packaging (createDistributable) only runs in release.yml.
+ # We disable coverage and desktop builds for PRs to keep feedback fast
+ # (< 10 mins). Desktop compilation is already covered by the :desktopApp:test
+ # task in the shard-app test shard.
validate-and-build:
needs: check-changes
if: needs.check-changes.outputs.android == 'true'
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 408c1f9eab..4f12cf5c8a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -150,9 +150,9 @@ jobs:
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
run: |
- rm -f ./app/google-services.json
- echo $GSERVICES > ./app/google-services.json
- echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
+ rm -f ./androidApp/google-services.json
+ echo $GSERVICES > ./androidApp/google-services.json
+ echo $KEYSTORE | base64 -di > ./androidApp/$KEYSTORE_FILENAME
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
echo "datadogApplicationId=$DATADOG_APPLICATION_ID" >> ./secrets.properties
echo "datadogClientToken=$DATADOG_CLIENT_TOKEN" >> ./secrets.properties
@@ -172,14 +172,14 @@ jobs:
run: bundle exec fastlane internal
- name: List outputs
- run: ls -R app/build/outputs/
+ run: ls -R androidApp/build/outputs/
- name: Upload Google AAB artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: google-aab
- path: app/build/outputs/bundle/googleRelease/app-google-release.aab
+ path: androidApp/build/outputs/bundle/googleRelease/androidApp-google-release.aab
retention-days: 1
- name: Upload Google APK artifact
@@ -187,20 +187,20 @@ jobs:
uses: actions/upload-artifact@v7
with:
name: google-apk
- path: app/build/outputs/apk/google/release/*.apk
+ path: androidApp/build/outputs/apk/google/release/*.apk
retention-days: 1
- name: Attest Google AAB provenance
if: success()
uses: actions/attest-build-provenance@v4
with:
- subject-path: app/build/outputs/bundle/googleRelease/app-google-release.aab
+ subject-path: androidApp/build/outputs/bundle/googleRelease/androidApp-google-release.aab
- name: Attest Google APK provenance
if: success()
uses: actions/attest-build-provenance@v4
with:
- subject-path: app/build/outputs/apk/google/release/*.apk
+ subject-path: androidApp/build/outputs/apk/google/release/*.apk
release-fdroid:
runs-on: ubuntu-24.04
@@ -229,7 +229,7 @@ jobs:
KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
run: |
- echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
+ echo $KEYSTORE | base64 -di > ./androidApp/$KEYSTORE_FILENAME
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
- name: Setup Fastlane
@@ -245,21 +245,21 @@ jobs:
run: bundle exec fastlane fdroid_build
- name: List outputs
- run: ls -R app/build/outputs/
+ run: ls -R androidApp/build/outputs/
- name: Upload F-Droid APK artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: fdroid-apk
- path: app/build/outputs/apk/fdroid/release/*.apk
+ path: androidApp/build/outputs/apk/fdroid/release/*.apk
retention-days: 1
- name: Attest F-Droid APK provenance
if: success()
uses: actions/attest-build-provenance@v4
with:
- subject-path: app/build/outputs/apk/fdroid/release/*.apk
+ subject-path: androidApp/build/outputs/apk/fdroid/release/*.apk
release-desktop:
if: ${{ inputs.build_desktop }}
@@ -307,13 +307,13 @@ jobs:
# `-PaboutLibraries.release=true` as member access on `-PaboutLibraries`,
# splitting the token and feeding `.release=true` to Gradle as a task name.
run: >
- ./gradlew :desktop:packageReleaseDistributionForCurrentOS
- ${{ contains(runner.os, 'macOS') && ':desktop:packageReleaseUberJarForCurrentOS' || '' }}
+ ./gradlew :desktopApp:packageReleaseDistributionForCurrentOS
+ ${{ contains(runner.os, 'macOS') && ':desktopApp:packageReleaseUberJarForCurrentOS' || '' }}
'-PaboutLibraries.release=true' --no-daemon
- name: List Desktop Binaries
if: runner.os == 'Linux' || runner.os == 'macOS'
- run: ls -R desktop/build/compose/binaries/main-release
+ run: ls -R desktopApp/build/compose/binaries/main-release
- name: Upload Desktop Artifacts
if: always()
@@ -321,13 +321,13 @@ jobs:
with:
name: desktop-${{ runner.os }}-${{ runner.arch }}
path: |
- desktop/build/compose/binaries/main-release/*/*.dmg
- desktop/build/compose/binaries/main-release/*/*.msi
- desktop/build/compose/binaries/main-release/*/*.exe
- desktop/build/compose/binaries/main-release/*/*.deb
- desktop/build/compose/binaries/main-release/*/*.rpm
- desktop/build/compose/binaries/main-release/*/*.AppImage
- desktop/build/compose/jars/*-release.jar
+ desktopApp/build/compose/binaries/main-release/*/*.dmg
+ desktopApp/build/compose/binaries/main-release/*/*.msi
+ desktopApp/build/compose/binaries/main-release/*/*.exe
+ desktopApp/build/compose/binaries/main-release/*/*.deb
+ desktopApp/build/compose/binaries/main-release/*/*.rpm
+ desktopApp/build/compose/binaries/main-release/*/*.AppImage
+ desktopApp/build/compose/jars/*-release.jar
retention-days: 1
if-no-files-found: ignore
@@ -336,13 +336,13 @@ jobs:
uses: actions/attest-build-provenance@v4
with:
subject-path: |
- desktop/build/compose/binaries/main-release/*/*.dmg
- desktop/build/compose/binaries/main-release/*/*.msi
- desktop/build/compose/binaries/main-release/*/*.exe
- desktop/build/compose/binaries/main-release/*/*.deb
- desktop/build/compose/binaries/main-release/*/*.rpm
- desktop/build/compose/binaries/main-release/*/*.AppImage
- desktop/build/compose/jars/*-release.jar
+ desktopApp/build/compose/binaries/main-release/*/*.dmg
+ desktopApp/build/compose/binaries/main-release/*/*.msi
+ desktopApp/build/compose/binaries/main-release/*/*.exe
+ desktopApp/build/compose/binaries/main-release/*/*.deb
+ desktopApp/build/compose/binaries/main-release/*/*.rpm
+ desktopApp/build/compose/binaries/main-release/*/*.AppImage
+ desktopApp/build/compose/jars/*-release.jar
create-flatpak-src:
if: ${{ inputs.build_flatpak_src }}
@@ -370,22 +370,7 @@ jobs:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache_read_only: 'true'
- - name: Python Setup
- uses: actions/setup-python@v6
- with:
- python-version: '3.x'
-
- - name: Install ast-grep
- run: pip install ast-grep-cli
-
- # Remove Android/iOS targets and other non-desktop bits
- # that would break the flatpakGradleGenerator
- - name: Prepare Offline Desktop Build
- run: ./scripts/desktop-only-prep.sh
-
- name: Generate Flatpak Sources
- env:
- DESKTOP_ONLY: true
run: >
./gradlew :build-logic:convention:flatpakGradleGenerator flatpakGradleGenerator
--no-configuration-cache --refresh-dependencies --no-parallel
diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml
index 74bb726ec9..c87b954c17 100644
--- a/.github/workflows/reusable-check.yml
+++ b/.github/workflows/reusable-check.yml
@@ -94,7 +94,7 @@ jobs:
- name: Lint, Analysis & KMP Smoke Compile
if: inputs.run_lint == true
- run: ./gradlew spotlessCheck detekt app:lintFdroidDebug app:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug kmpSmokeCompile -Pci=true --continue --scan
+ run: ./gradlew spotlessCheck detekt androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug kmpSmokeCompile -Pci=true --continue --scan
- name: KMP Smoke Compile (lint skipped)
if: inputs.run_lint == false
@@ -152,13 +152,13 @@ jobs:
# See: https://github.com/meshtastic/Meshtastic-Android/issues/3231
echo "ββ Step 1: Verify aboutlibraries.json determinism ββ"
- rm -f app/src/main/resources/aboutlibraries.json
- ./gradlew :app:exportLibraryDefinitions -Pci=true --no-configuration-cache
- cp app/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run1.json
+ rm -f androidApp/src/main/resources/aboutlibraries.json
+ ./gradlew :androidApp:exportLibraryDefinitions -Pci=true --no-configuration-cache
+ cp androidApp/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run1.json
- rm -f app/src/main/resources/aboutlibraries.json
- ./gradlew :app:exportLibraryDefinitions -Pci=true --no-configuration-cache --rerun-tasks
- cp app/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run2.json
+ rm -f androidApp/src/main/resources/aboutlibraries.json
+ ./gradlew :androidApp:exportLibraryDefinitions -Pci=true --no-configuration-cache --rerun-tasks
+ cp androidApp/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run2.json
if ! diff -q /tmp/aboutlibraries-run1.json /tmp/aboutlibraries-run2.json; then
echo "::error::aboutlibraries.json is NOT deterministic across runs!"
@@ -168,9 +168,9 @@ jobs:
echo "β
aboutlibraries.json is deterministic"
echo "ββ Step 2: Build fdroid release APK ββ"
- ./gradlew :app:assembleFdroidRelease -Pci=true -Pmeshtastic.disableAbiSplits=true --no-configuration-cache
+ ./gradlew :androidApp:assembleFdroidRelease -Pci=true -Pmeshtastic.disableAbiSplits=true --no-configuration-cache
- APK=$(find app/build/outputs/apk/fdroid/release -name "*.apk" | head -1)
+ APK=$(find androidApp/build/outputs/apk/fdroid/release -name "*.apk" | head -1)
if [ -z "$APK" ]; then
echo "::error::No fdroid release APK found"
exit 1
@@ -295,7 +295,7 @@ jobs:
# Tests are split into 3 shards that run in parallel:
# shard-core: core:* KMP module tests (allTests)
# shard-feature: feature:* KMP module tests (allTests)
- # shard-app: Pure-Android/JVM tests (app, desktop, core:barcode, etc.)
+ # shard-app: Pure-Android/JVM tests (androidApp, desktopApp, core:barcode, etc.)
test-shards:
runs-on: ubuntu-24.04
permissions:
@@ -359,17 +359,17 @@ jobs:
:feature:settings:koverXmlReport
- name: shard-app
tasks: >-
- :app:testFdroidDebugUnitTest
- :app:testGoogleDebugUnitTest
- :desktop:test
+ :androidApp:testFdroidDebugUnitTest
+ :androidApp:testGoogleDebugUnitTest
+ :desktopApp:test
:core:barcode:testFdroidDebugUnitTest
:core:barcode:testGoogleDebugUnitTest
kover: >-
- :app:koverXmlReportFdroidDebug
- :app:koverXmlReportGoogleDebug
+ :androidApp:koverXmlReportFdroidDebug
+ :androidApp:koverXmlReportGoogleDebug
:core:barcode:koverXmlReportFdroidDebug
:core:barcode:koverXmlReportGoogleDebug
- :desktop:koverXmlReport
+ :desktopApp:koverXmlReport
steps:
- name: Checkout code
@@ -447,14 +447,14 @@ jobs:
cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
- name: Build Android APKs
- run: ./gradlew app:assembleFdroidDebug app:assembleGoogleDebug -Pci=true --parallel --configuration-cache --continue --scan
+ run: ./gradlew androidApp:assembleFdroidDebug androidApp:assembleGoogleDebug -Pci=true --parallel --configuration-cache --continue --scan
- name: Upload debug artifact
if: ${{ inputs.upload_artifacts }}
uses: actions/upload-artifact@v7
with:
name: app-debug-apks
- path: app/build/outputs/apk/*/debug/*.apk
+ path: androidApp/build/outputs/apk/*/debug/*.apk
retention-days: 7
- name: Report App Size
@@ -463,8 +463,47 @@ jobs:
echo "### App Size Report" >> $GITHUB_STEP_SUMMARY
echo "| Artifact | Size |" >> $GITHUB_STEP_SUMMARY
echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY
- find app/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}' >> $GITHUB_STEP_SUMMARY
+ find androidApp/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}' >> $GITHUB_STEP_SUMMARY
+ # ββ Desktop Build βββββββββββββββββββββββββββββββββββββββββββββββββββ
+ build-desktop:
+ name: Build Desktop Debug (${{ matrix.os }})
+ if: inputs.run_desktop_builds == true
+ runs-on: ${{ matrix.os }}
+ permissions:
+ contents: read
+ timeout-minutes: 60
+ needs: lint-check
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]
+ env:
+ VERSION_CODE: ${{ needs.lint-check.outputs.version_code }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 1
+ submodules: true
+
+ - name: Gradle Setup
+ uses: ./.github/actions/gradle-setup
+ with:
+ gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
+ cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
+
+ - name: Build Desktop
+ run: ./gradlew :desktopApp:createDistributable -Pci=true --scan
+
+ - name: Upload Desktop artifact
+ if: ${{ inputs.upload_artifacts }}
+ uses: actions/upload-artifact@v7
+ with:
+ name: desktop-app-${{ runner.os }}-${{ runner.arch }}
+ path: desktopApp/build/compose/binaries/main/app/
+ retention-days: 7
# ββ Flatpak Sources βββββββββββββββββββββββββββββββββββββββββββββββββββ
build-flatpak-src:
@@ -494,23 +533,8 @@ jobs:
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache_read_only: true
-
- - name: Python Setup
- uses: actions/setup-python@v6
- with:
- python-version: '3.x'
-
- - name: Install ast-grep
- run: pip install ast-grep-cli
-
- # Remove Android/iOS targets and other non-desktop bits
- # that would break the flatpakGradleGenerator
- - name: Prepare Offline Desktop Build
- run: ./scripts/desktop-only-prep.sh
- name: Generate Flatpak Sources
- env:
- DESKTOP_ONLY: true
run: >
./gradlew :build-logic:convention:flatpakGradleGenerator flatpakGradleGenerator
--no-configuration-cache --refresh-dependencies --no-parallel
diff --git a/.github/workflows/scheduled-updates.yml b/.github/workflows/scheduled-updates.yml
index 06f7a9e84b..216d05c4eb 100644
--- a/.github/workflows/scheduled-updates.yml
+++ b/.github/workflows/scheduled-updates.yml
@@ -22,7 +22,7 @@ jobs:
- name: Update firmware releases list
id: firmware
run: |
- firmware_file_path="app/src/main/assets/firmware_releases.json"
+ firmware_file_path="androidApp/src/main/assets/firmware_releases.json"
temp_firmware_file="/tmp/new_firmware_releases.json"
echo "Fetching latest firmware releases..."
@@ -51,7 +51,7 @@ jobs:
- name: Update hardware list
id: hardware
run: |
- hardware_file_path="app/src/main/assets/device_hardware.json"
+ hardware_file_path="androidApp/src/main/assets/device_hardware.json"
temp_hardware_file="/tmp/new_device_hardware.json"
echo "Fetching latest device hardware data..."
@@ -172,8 +172,8 @@ jobs:
base: 'main'
delete-branch: true
add-paths: |
- app/src/main/assets/firmware_releases.json
- app/src/main/assets/device_hardware.json
+ androidApp/src/main/assets/firmware_releases.json
+ androidApp/src/main/assets/device_hardware.json
fastlane/metadata/android/**
**/strings.xml
**/README.md
diff --git a/.skills/code-review/SKILL.md b/.skills/code-review/SKILL.md
index b9442e07da..286d65ec95 100644
--- a/.skills/code-review/SKILL.md
+++ b/.skills/code-review/SKILL.md
@@ -23,8 +23,8 @@ When reviewing code, meticulously verify the following categories. Flag any devi
- [ ] **Compose Multiplatform Resources:** Ensure NO hardcoded strings. Must use `core:resources` (e.g., `stringResource(Res.string.key)` or asynchronous `getStringSuspend(Res.string.key)` for ViewModels/Coroutines). NEVER use blocking `getString()` in a coroutine.
- [ ] **String Formatting:** CMP only supports `%N$s` and `%N$d`. Flag any float formats (`%N$.1f`) in Compose string resources; they must be pre-formatted using `NumberFormatter.format()` from `core:common`. Use `MetricFormatter` for metric-specific displays (temperature, voltage, current, percent, humidity, pressure, SNR, RSSI).
- [ ] **Centralized Dialogs & Alerts:** Flag inline alert-rendering logic. Mandate the use of `AlertHost(alertManager)` or `SharedDialogs` from `core:ui/commonMain`.
-- [ ] **Placeholders:** Require `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktop/JVM features. No inline placeholders in feature modules.
-- [ ] **Adaptive Layouts:** Verify use of `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support desktop/tablet breakpoints (β₯ 1200dp).
+- [ ] **Placeholders:** Require `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktopApp/JVM features. No inline placeholders in feature modules.
+- [ ] **Adaptive Layouts:** Verify use of `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support desktopApp/tablet breakpoints (β₯ 1200dp).
### 3. Navigation & State
- [ ] **Shared Navigation Graphs:** Feature navigation graphs must be defined as extension functions on `EntryProviderScope<NavKey>` in `commonMain` (e.g., `fun EntryProviderScope<NavKey>.settingsGraph(...)`). Flag any graphs defined in platform-specific source sets.
@@ -56,8 +56,8 @@ When reviewing code, meticulously verify the following categories. Flag any devi
- [ ] **Robolectric Configuration:** Check that Compose UI tests running via Robolectric on JVM are pinned to `@Config(sdk = [34])` to prevent Java 21 / SDK 35 compatibility issues.
### 8. ProGuard / R8 Rules
-- [ ] **New Dependencies:** If a new reflection-heavy dependency is added (DI, serialization, JNI, ServiceLoader), verify keep rules exist in **both** `app/proguard-rules.pro` (R8) and `desktop/proguard-rules.pro` (ProGuard). The two files must stay aligned.
-- [ ] **Release Smoke-Test:** For dependency or ProGuard rule changes, verify `assembleRelease` and `./gradlew :desktop:runRelease` succeed.
+- [ ] **New Dependencies:** If a new reflection-heavy dependency is added (DI, serialization, JNI, ServiceLoader), verify keep rules exist in **both** `androidApp/proguard-rules.pro` (R8) and `desktopApp/proguard-rules.pro` (ProGuard). The two files must stay aligned.
+- [ ] **Release Smoke-Test:** For dependency or ProGuard rule changes, verify `assembleRelease` and `./gradlew :desktopApp:runRelease` succeed.
## Review Output Guidelines
1. **Be Specific & Constructive:** Provide exact file references and code snippets illustrating the required project pattern.
diff --git a/.skills/compose-ui/SKILL.md b/.skills/compose-ui/SKILL.md
index 22fe1b489e..3249cb6e02 100644
--- a/.skills/compose-ui/SKILL.md
+++ b/.skills/compose-ui/SKILL.md
@@ -4,9 +4,9 @@
Guidelines for building shared UI, adaptive layouts, and handling strings/resources in Meshtastic-Android. The codebase uses Material 3 Adaptive.
## 1. UI Components & Layouts
-- **Material 3 / Adaptive:** Use `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support Large (1200dp) and XL (1600dp) breakpoints. Investigate 3-pane "Power User" scenes using Navigation 3 Scenes and draggable dividers for desktop/tablets.
+- **Material 3 / Adaptive:** Use `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support Large (1200dp) and XL (1600dp) breakpoints. Investigate 3-pane "Power User" scenes using Navigation 3 Scenes and draggable dividers for desktopApp/tablets.
- **Dialogs & Alerts:** Use centralized components like `AlertHost(alertManager)` from `core:ui/commonMain`. Do NOT trigger alerts inline or duplicate alert logic. Use `SharedDialogs(uiViewModel)` for general popups.
-- **Placeholders:** Use `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktop/JVM features.
+- **Placeholders:** Use `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktopApp/JVM features.
- **Theme Picker:** Use `ThemePickerDialog` from `feature:settings/commonMain`.
- **Platform Implementations:** Inject platform-specific behavior (e.g., Map providers) via `CompositionLocal` from the `app` or `desktop` shells. Do not tightly couple Google Maps/osmdroid dependencies to `commonMain`.
@@ -58,4 +58,4 @@ Choose the right tool for the job:
## Reference Anchors
- **Shared Strings:** `core/resources/src/commonMain/composeResources/values/strings.xml`
- **Platform abstraction contract:** `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/MapViewProvider.kt`
-- **Provider wiring:** `app/src/main/kotlin/org/meshtastic/app/MainActivity.kt`
+- **Provider wiring:** `androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt`
diff --git a/.skills/implement-feature/SKILL.md b/.skills/implement-feature/SKILL.md
index 0277bee10a..f6558d37fa 100644
--- a/.skills/implement-feature/SKILL.md
+++ b/.skills/implement-feature/SKILL.md
@@ -35,7 +35,7 @@ A step-by-step workflow for implementing a new feature in the Meshtastic-Android
```bash
./gradlew spotlessCheck spotlessApply detekt assembleDebug test allTests
```
-- If the feature adds a new reflection-heavy dependency, add keep rules to **both** `app/proguard-rules.pro` and `desktop/proguard-rules.pro`, then verify release builds:
+- If the feature adds a new reflection-heavy dependency, add keep rules to **both** `androidApp/proguard-rules.pro` and `desktopApp/proguard-rules.pro`, then verify release builds:
```bash
- ./gradlew assembleFdroidRelease :desktop:runRelease
+ ./gradlew assembleFdroidRelease :desktopApp:runRelease
```
diff --git a/.skills/kmp-architecture/SKILL.md b/.skills/kmp-architecture/SKILL.md
index 46602c4307..3a4ff0d3cc 100644
--- a/.skills/kmp-architecture/SKILL.md
+++ b/.skills/kmp-architecture/SKILL.md
@@ -52,10 +52,10 @@ Guidelines on managing Kotlin Multiplatform (KMP) source-sets, expected abstract
1. Ensure all new logic compiles against the KMP core (`jvm()`, `iosArm64()`, etc.).
2. Do not use platform-specific constructs in `commonMain` or you break the iOS/Desktop builds.
3. Test using `kmpSmokeCompile` to verify cross-platform compilation.
-4. For desktop wiring, copy the pattern in `desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt` and use `NoopStubs.kt` to temporarily mock missing platform implementations.
+4. For desktop wiring, copy the pattern in `desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt` and use `NoopStubs.kt` to temporarily mock missing platform implementations.
## Reference Anchors
- **Shared Okio I/O:** `core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt`
-- **Desktop DI Stubs:** `desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt`
+- **Desktop DI Stubs:** `desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt`
- **Version Catalog:** `gradle/libs.versions.toml`
- **Convention Plugins:** `build-logic/convention/`
diff --git a/.skills/navigation-and-di/SKILL.md b/.skills/navigation-and-di/SKILL.md
index c9d7336a6e..4119a91a08 100644
--- a/.skills/navigation-and-di/SKILL.md
+++ b/.skills/navigation-and-di/SKILL.md
@@ -7,7 +7,7 @@ This skill covers dependency injection (Koin Annotations 4.2.x) and JetBrains Na
### Guidelines
1. **Annotations First:** Use `@Module`, `@ComponentScan`, and `@KoinViewModel` annotations directly in `commonMain` shared modules to encapsulate dependency graphs per feature.
-2. **App Root Assembly:** Don't assume feature/core `@Module` classes are active automatically. Ensure they are included by the app root module (`@Module(includes = [...])`) in `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt` and `desktop/.../DesktopKoinModule.kt`.
+2. **App Root Assembly:** Don't assume feature/core `@Module` classes are active automatically. Ensure they are included by the app root module (`@Module(includes = [...])`) in `androidApp/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt` and `desktopApp/.../DesktopKoinModule.kt`.
3. **No Platform Bleed:** Don't put Android framework dependencies (`Context`, `Activity`, `Application`) into shared `commonMain` business logic. Inject interfaces instead.
4. **Resolution:** Resolve app-layer wrappers via `koinViewModel()` or injected bindings within Compose navigation graphs.
@@ -49,8 +49,8 @@ startKoin<AndroidKoinApp> {
- **Custom Backstack Mutation:** Do **not** mutate back navigation with custom stacks disconnected from the app backstack. Mutate `NavBackStack<NavKey>` directly with `add(...)` and `removeLastOrNull()`.
## Reference Anchors
-- **App Startup / Koin Bootstrap:** `app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt`
-- **DI Bootstrap Object:** `app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt`
-- **DI App Wiring:** `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt`
+- **App Startup / Koin Bootstrap:** `androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt`
+- **DI Bootstrap Object:** `androidApp/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt`
+- **DI App Wiring:** `androidApp/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt`
- **Shared Routes:** `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt`
-- **Desktop Nav Shell:** `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
+- **Desktop Nav Shell:** `desktopApp/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
diff --git a/.skills/project-overview/SKILL.md b/.skills/project-overview/SKILL.md
index 2224fa7ade..6c8133ad8b 100644
--- a/.skills/project-overview/SKILL.md
+++ b/.skills/project-overview/SKILL.md
@@ -11,7 +11,7 @@ Module directory, namespacing conventions, environment setup, and troubleshootin
| Directory | Description |
| :--- | :--- |
-| `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
+| `androidApp/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.kmp.jvm.android`, `meshtastic.koin`). |
| `config/` | Detekt static analysis rules (`config/detekt/detekt.yml`) and Spotless formatting config (`config/spotless/.editorconfig`). |
| `docs/` | Architecture docs and agent playbooks. See `docs/kmp-status.md` and `docs/roadmap.md` for current status. |
@@ -38,7 +38,7 @@ Module directory, namespacing conventions, environment setup, and troubleshootin
| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`, `firmware`, `wifi-provision`, `widget`). All are KMP except `widget`. Use `meshtastic.kmp.feature` convention plugin. |
| `feature/wifi-provision` | KMP WiFi provisioning via BLE (Nymea protocol). Uses `core:ble` Kable abstractions. |
| `feature/firmware` | Fully KMP firmware update system: Unified OTA (BLE + WiFi), native Nordic Secure DFU protocol (pure KMP), USB/UF2 updates, and `FirmwareRetriever` with manifest-based resolution. Desktop is a first-class target. |
-| `desktop/` | Compose Desktop application. Thin host shell relying on feature modules for shared UI. Full Koin DI graph, TCP, Serial/USB, and BLE transports. Versioning via `config.properties` + `GitVersionValueSource`. |
+| `desktopApp/` | Compose Desktop application. Thin host shell relying on feature modules for shared UI. Full Koin DI graph, TCP, Serial/USB, and BLE transports. Versioning via `config.properties` + `GitVersionValueSource`. |
## Namespacing
- **Standard:** Use the `org.meshtastic.*` namespace for all code.
diff --git a/.skills/testing-ci/SKILL.md b/.skills/testing-ci/SKILL.md
index bb98579669..4144354c33 100644
--- a/.skills/testing-ci/SKILL.md
+++ b/.skills/testing-ci/SKILL.md
@@ -17,7 +17,7 @@ Run in a single invocation for routine changes to ensure code formatting, analys
> In KMP modules, the `test` task name is **ambiguous**. Gradle matches both `testAndroid` and
> `testAndroidHostTest` and refuses to run either, silently skipping KMP modules.
> `allTests` is the `KotlinTestReport` lifecycle task registered by the KMP plugin.
-> Conversely, `allTests` does **not** cover pure-Android modules (`:app`, `:core:api`, etc.), which is why both `test` and `allTests` are needed.
+> Conversely, `allTests` does **not** cover pure-Android modules (`:androidApp`, `:core:api`, etc.), which is why both `test` and `allTests` are needed.
*Note: If testing Compose UI on the JVM (Robolectric) with Java 21, pin tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
diff --git a/.specify/feature.json b/.specify/feature.json
index c28b048e07..c800a63214 100644
--- a/.specify/feature.json
+++ b/.specify/feature.json
@@ -1 +1 @@
-{"feature_directory":"specs/20260511-211823-compose-screenshot-testing"}
+{"feature_directory":"specs/006-kmp-project-structure"}
diff --git a/AGENTS.md b/AGENTS.md
index 75f08c34fb..f7d3bdcd63 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -49,5 +49,5 @@ You are an expert Android/KMP engineer. Maintain architectural boundaries, use M
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan
-at `specs/20260511-211823-compose-screenshot-testing/plan.md`
+at `specs/006-kmp-project-structure/plan.md`
<!-- SPECKIT END -->
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d4fe0b7407..f0b3ec49c0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -20,7 +20,7 @@ Thank you for your interest in contributing to Meshtastic-Android! We welcome co
- Add comments where necessary, especially for complex logic.
- Keep methods and classes focused and concise.
- **Strings:** Use localised strings via the **Compose Multiplatform Resource** library in `:core:resources`.
- - Do **not** use the legacy `app/src/main/res/values/strings.xml`.
+ - Do **not** use the legacy `androidApp/src/main/res/values/strings.xml`.
- **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`.
- **Usage:**
```kotlin
diff --git a/app/README.md b/androidApp/README.md
similarity index 54%
rename from app/README.md
rename to androidApp/README.md
index ff6f5542ff..cc4b8cafea 100644
--- a/app/README.md
+++ b/androidApp/README.md
@@ -1,7 +1,7 @@
-# `:app`
+# `:androidApp`
## Overview
-The `:app` module is the entry point for the Meshtastic Android application. It orchestrates the various feature modules, manages global state, and provides the main UI shell.
+The `:androidApp` module is the entry point for the Meshtastic Android application. It orchestrates the various feature modules, manages global state, and provides the main UI shell.
## Key Components
@@ -9,7 +9,7 @@ The `:app` module is the entry point for the Meshtastic Android application. It
The single Activity of the application. It hosts the shared `MeshtasticNavDisplay` navigation shell and manages the root UI structure (Navigation Bar, Rail, etc.).
### 2. `MeshService`
-The core background service that manages long-running communication with the mesh radio. While it is declared in the `:app` manifest for system visibility, its implementation resides in the `:core:service` module. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background.
+The core background service that manages long-running communication with the mesh radio. While it is declared in the `:androidApp` manifest for system visibility, its implementation resides in the `:core:service` module. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background.
### 3. Koin Application
`MeshUtilApplication` is the Koin entry point, providing the global dependency injection container.
@@ -24,34 +24,34 @@ The module primarily serves as a "glue" layer, connecting:
<!--region graph-->
```mermaid
graph TB
- :app[app]:::android-application
- :app -.-> :core:ble
- :app -.-> :core:common
- :app -.-> :core:data
- :app -.-> :core:database
- :app -.-> :core:datastore
- :app -.-> :core:di
- :app -.-> :core:domain
- :app -.-> :core:model
- :app -.-> :core:navigation
- :app -.-> :core:network
- :app -.-> :core:nfc
- :app -.-> :core:prefs
- :app -.-> :core:proto
- :app -.-> :core:service
- :app -.-> :core:resources
- :app -.-> :core:ui
- :app -.-> :core:barcode
- :app -.-> :core:takserver
- :app -.-> :feature:intro
- :app -.-> :feature:messaging
- :app -.-> :feature:connections
- :app -.-> :feature:map
- :app -.-> :feature:node
- :app -.-> :feature:settings
- :app -.-> :feature:firmware
- :app -.-> :feature:wifi-provision
- :app -.-> :feature:widget
+ :androidApp[androidApp]:::android-application
+ :androidApp -.-> :core:ble
+ :androidApp -.-> :core:common
+ :androidApp -.-> :core:data
+ :androidApp -.-> :core:database
+ :androidApp -.-> :core:datastore
+ :androidApp -.-> :core:di
+ :androidApp -.-> :core:domain
+ :androidApp -.-> :core:model
+ :androidApp -.-> :core:navigation
+ :androidApp -.-> :core:network
+ :androidApp -.-> :core:nfc
+ :androidApp -.-> :core:prefs
+ :androidApp -.-> :core:proto
+ :androidApp -.-> :core:service
+ :androidApp -.-> :core:resources
+ :androidApp -.-> :core:ui
+ :androidApp -.-> :core:barcode
+ :androidApp -.-> :core:takserver
+ :androidApp -.-> :feature:intro
+ :androidApp -.-> :feature:messaging
+ :androidApp -.-> :feature:connections
+ :androidApp -.-> :feature:map
+ :androidApp -.-> :feature:node
+ :androidApp -.-> :feature:settings
+ :androidApp -.-> :feature:firmware
+ :androidApp -.-> :feature:wifi-provision
+ :androidApp -.-> :feature:widget
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
diff --git a/app/build.gradle.kts b/androidApp/build.gradle.kts
similarity index 99%
rename from app/build.gradle.kts
rename to androidApp/build.gradle.kts
index 45de5390d9..8581f519f7 100644
--- a/app/build.gradle.kts
+++ b/androidApp/build.gradle.kts
@@ -116,7 +116,7 @@ configure<ApplicationExtension> {
}
// Disable ABI splits for bundle builds or when explicitly requested via Gradle property.
- // Usage: ./gradlew :app:bundleGoogleRelease -Pmeshtastic.disableAbiSplits=true
+ // Usage: ./gradlew :androidApp:bundleGoogleRelease -Pmeshtastic.disableAbiSplits=true
val disableSplits = providers.gradleProperty("meshtastic.disableAbiSplits").map { it.toBoolean() }.getOrElse(false)
// Enable ABI splits to generate smaller APKs per architecture for F-Droid/IzzyOnDroid
diff --git a/app/detekt-baseline.xml b/androidApp/detekt-baseline.xml
similarity index 100%
rename from app/detekt-baseline.xml
rename to androidApp/detekt-baseline.xml
diff --git a/app/google-services.json b/androidApp/google-services.json
similarity index 100%
rename from app/google-services.json
rename to androidApp/google-services.json
diff --git a/app/proguard-rules.pro b/androidApp/proguard-rules.pro
similarity index 100%
rename from app/proguard-rules.pro
rename to androidApp/proguard-rules.pro
diff --git a/app/src/fdroid/AndroidManifest.xml b/androidApp/src/fdroid/AndroidManifest.xml
similarity index 100%
rename from app/src/fdroid/AndroidManifest.xml
rename to androidApp/src/fdroid/AndroidManifest.xml
diff --git a/app/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java b/androidApp/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java
similarity index 100%
rename from app/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java
rename to androidApp/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java
diff --git a/app/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java b/androidApp/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java
similarity index 100%
rename from app/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java
rename to androidApp/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java
diff --git a/app/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java b/androidApp/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java
similarity index 100%
rename from app/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java
rename to androidApp/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapUtils.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapUtils.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/MapUtils.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapUtils.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/SqlTileWriterExt.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/SqlTileWriterExt.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/SqlTileWriterExt.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/SqlTileWriterExt.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/component/CacheLayout.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/CacheLayout.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/component/CacheLayout.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/CacheLayout.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/component/DownloadButton.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/DownloadButton.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/component/DownloadButton.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/DownloadButton.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/model/MarkerWithLabel.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/MarkerWithLabel.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/model/MarkerWithLabel.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/MarkerWithLabel.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/model/OnlineTileSourceAuth.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/OnlineTileSourceAuth.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/model/OnlineTileSourceAuth.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/OnlineTileSourceAuth.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackOsmMap.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackOsmMap.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackOsmMap.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackOsmMap.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteOsmMap.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteOsmMap.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteOsmMap.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteOsmMap.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt
similarity index 100%
rename from app/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt
rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt
diff --git a/app/src/fdroid/res/drawable/ic_location_on.xml b/androidApp/src/fdroid/res/drawable/ic_location_on.xml
similarity index 100%
rename from app/src/fdroid/res/drawable/ic_location_on.xml
rename to androidApp/src/fdroid/res/drawable/ic_location_on.xml
diff --git a/app/src/fdroid/res/drawable/ic_map_location_dot.xml b/androidApp/src/fdroid/res/drawable/ic_map_location_dot.xml
similarity index 100%
rename from app/src/fdroid/res/drawable/ic_map_location_dot.xml
rename to androidApp/src/fdroid/res/drawable/ic_map_location_dot.xml
diff --git a/app/src/fdroid/res/drawable/ic_map_navigation.xml b/androidApp/src/fdroid/res/drawable/ic_map_navigation.xml
similarity index 100%
rename from app/src/fdroid/res/drawable/ic_map_navigation.xml
rename to androidApp/src/fdroid/res/drawable/ic_map_navigation.xml
diff --git a/app/src/fdroidDebug/res/drawable-anydpi/ic_launcher_background.xml b/androidApp/src/fdroidDebug/res/drawable-anydpi/ic_launcher_background.xml
similarity index 100%
rename from app/src/fdroidDebug/res/drawable-anydpi/ic_launcher_background.xml
rename to androidApp/src/fdroidDebug/res/drawable-anydpi/ic_launcher_background.xml
diff --git a/app/src/fdroidDebug/res/values/strings.xml b/androidApp/src/fdroidDebug/res/values/strings.xml
similarity index 100%
rename from app/src/fdroidDebug/res/values/strings.xml
rename to androidApp/src/fdroidDebug/res/values/strings.xml
diff --git a/app/src/google/AndroidManifest.xml b/androidApp/src/google/AndroidManifest.xml
similarity index 100%
rename from app/src/google/AndroidManifest.xml
rename to androidApp/src/google/AndroidManifest.xml
diff --git a/app/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt b/androidApp/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt b/androidApp/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt b/androidApp/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt b/androidApp/src/google/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MBTilesProvider.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MBTilesProvider.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/MBTilesProvider.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/MBTilesProvider.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/MapView.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/CustomMapLayersSheet.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/CustomMapLayersSheet.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/component/CustomMapLayersSheet.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/CustomMapLayersSheet.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/PulsingNodeChip.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/PulsingNodeChip.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/component/PulsingNodeChip.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/PulsingNodeChip.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileProviderConfig.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/model/CustomTileProviderConfig.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileProviderConfig.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/model/CustomTileProviderConfig.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt b/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt
diff --git a/app/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt b/androidApp/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt
similarity index 100%
rename from app/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt
rename to androidApp/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt
diff --git a/app/src/googleDebug/res/drawable-anydpi/ic_launcher_background.xml b/androidApp/src/googleDebug/res/drawable-anydpi/ic_launcher_background.xml
similarity index 100%
rename from app/src/googleDebug/res/drawable-anydpi/ic_launcher_background.xml
rename to androidApp/src/googleDebug/res/drawable-anydpi/ic_launcher_background.xml
diff --git a/app/src/googleDebug/res/values/strings.xml b/androidApp/src/googleDebug/res/values/strings.xml
similarity index 100%
rename from app/src/googleDebug/res/values/strings.xml
rename to androidApp/src/googleDebug/res/values/strings.xml
diff --git a/app/src/main/.gitignore b/androidApp/src/main/.gitignore
similarity index 100%
rename from app/src/main/.gitignore
rename to androidApp/src/main/.gitignore
diff --git a/app/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml
similarity index 100%
rename from app/src/main/AndroidManifest.xml
rename to androidApp/src/main/AndroidManifest.xml
diff --git a/app/src/main/assets/device_bootloader_ota_quirks.json b/androidApp/src/main/assets/device_bootloader_ota_quirks.json
similarity index 100%
rename from app/src/main/assets/device_bootloader_ota_quirks.json
rename to androidApp/src/main/assets/device_bootloader_ota_quirks.json
diff --git a/app/src/main/assets/device_hardware.json b/androidApp/src/main/assets/device_hardware.json
similarity index 100%
rename from app/src/main/assets/device_hardware.json
rename to androidApp/src/main/assets/device_hardware.json
diff --git a/app/src/main/assets/firmware_releases.json b/androidApp/src/main/assets/firmware_releases.json
similarity index 100%
rename from app/src/main/assets/firmware_releases.json
rename to androidApp/src/main/assets/firmware_releases.json
diff --git a/app/src/main/ic_launcher-114x114.png b/androidApp/src/main/ic_launcher-114x114.png
similarity index 100%
rename from app/src/main/ic_launcher-114x114.png
rename to androidApp/src/main/ic_launcher-114x114.png
diff --git a/app/src/main/ic_launcher2-playstore.png b/androidApp/src/main/ic_launcher2-playstore.png
similarity index 100%
rename from app/src/main/ic_launcher2-playstore.png
rename to androidApp/src/main/ic_launcher2-playstore.png
diff --git a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt b/androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt
similarity index 100%
rename from app/src/main/kotlin/org/meshtastic/app/MainActivity.kt
rename to androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt
diff --git a/app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt b/androidApp/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt
similarity index 100%
rename from app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt
rename to androidApp/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt
diff --git a/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt b/androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt
similarity index 100%
rename from app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt
rename to androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt b/androidApp/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt
similarity index 100%
rename from app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt
rename to androidApp/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt b/androidApp/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt
similarity index 100%
rename from app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt
rename to androidApp/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt b/androidApp/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt
similarity index 100%
rename from app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt
rename to androidApp/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt
diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt b/androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt
similarity index 100%
rename from app/src/main/kotlin/org/meshtastic/app/ui/Main.kt
rename to androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt
diff --git a/app/src/main/res/drawable-anydpi/ic_launcher_background.xml b/androidApp/src/main/res/drawable-anydpi/ic_launcher_background.xml
similarity index 100%
rename from app/src/main/res/drawable-anydpi/ic_launcher_background.xml
rename to androidApp/src/main/res/drawable-anydpi/ic_launcher_background.xml
diff --git a/app/src/main/res/drawable-anydpi/ic_launcher_foreground.xml b/androidApp/src/main/res/drawable-anydpi/ic_launcher_foreground.xml
similarity index 100%
rename from app/src/main/res/drawable-anydpi/ic_launcher_foreground.xml
rename to androidApp/src/main/res/drawable-anydpi/ic_launcher_foreground.xml
diff --git a/app/src/main/res/drawable-anydpi/ic_splash.xml b/androidApp/src/main/res/drawable-anydpi/ic_splash.xml
similarity index 100%
rename from app/src/main/res/drawable-anydpi/ic_splash.xml
rename to androidApp/src/main/res/drawable-anydpi/ic_splash.xml
diff --git a/app/src/main/res/layout/widget_local_stats_preview.xml b/androidApp/src/main/res/layout/widget_local_stats_preview.xml
similarity index 100%
rename from app/src/main/res/layout/widget_local_stats_preview.xml
rename to androidApp/src/main/res/layout/widget_local_stats_preview.xml
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/androidApp/src/main/res/mipmap-anydpi/ic_launcher.xml
similarity index 100%
rename from app/src/main/res/mipmap-anydpi/ic_launcher.xml
rename to androidApp/src/main/res/mipmap-anydpi/ic_launcher.xml
diff --git a/app/src/main/res/raw/keep.xml b/androidApp/src/main/res/raw/keep.xml
similarity index 100%
rename from app/src/main/res/raw/keep.xml
rename to androidApp/src/main/res/raw/keep.xml
diff --git a/app/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml
similarity index 100%
rename from app/src/main/res/values/strings.xml
rename to androidApp/src/main/res/values/strings.xml
diff --git a/app/src/main/res/values/styles.xml b/androidApp/src/main/res/values/styles.xml
similarity index 100%
rename from app/src/main/res/values/styles.xml
rename to androidApp/src/main/res/values/styles.xml
diff --git a/app/src/main/res/xml/automotive_app_desc.xml b/androidApp/src/main/res/xml/automotive_app_desc.xml
similarity index 100%
rename from app/src/main/res/xml/automotive_app_desc.xml
rename to androidApp/src/main/res/xml/automotive_app_desc.xml
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/androidApp/src/main/res/xml/data_extraction_rules.xml
similarity index 100%
rename from app/src/main/res/xml/data_extraction_rules.xml
rename to androidApp/src/main/res/xml/data_extraction_rules.xml
diff --git a/app/src/main/res/xml/device_filter.xml b/androidApp/src/main/res/xml/device_filter.xml
similarity index 100%
rename from app/src/main/res/xml/device_filter.xml
rename to androidApp/src/main/res/xml/device_filter.xml
diff --git a/app/src/main/res/xml/locales_config.xml b/androidApp/src/main/res/xml/locales_config.xml
similarity index 100%
rename from app/src/main/res/xml/locales_config.xml
rename to androidApp/src/main/res/xml/locales_config.xml
diff --git a/app/src/main/res/xml/network_security_config.xml b/androidApp/src/main/res/xml/network_security_config.xml
similarity index 100%
rename from app/src/main/res/xml/network_security_config.xml
rename to androidApp/src/main/res/xml/network_security_config.xml
diff --git a/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt
similarity index 100%
rename from app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt
rename to androidApp/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt
diff --git a/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt b/androidApp/src/test/kotlin/org/meshtastic/app/service/Fakes.kt
similarity index 100%
rename from app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt
rename to androidApp/src/test/kotlin/org/meshtastic/app/service/Fakes.kt
diff --git a/app/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt
similarity index 100%
rename from app/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt
rename to androidApp/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt
diff --git a/app/src/test/kotlin/org/meshtastic/app/ui/UIUnitTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/ui/UIUnitTest.kt
similarity index 100%
rename from app/src/test/kotlin/org/meshtastic/app/ui/UIUnitTest.kt
rename to androidApp/src/test/kotlin/org/meshtastic/app/ui/UIUnitTest.kt
diff --git a/app/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt
similarity index 100%
rename from app/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt
rename to androidApp/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt
diff --git a/app/src/test/resources/robolectric.properties b/androidApp/src/test/resources/robolectric.properties
similarity index 100%
rename from app/src/test/resources/robolectric.properties
rename to androidApp/src/test/resources/robolectric.properties
diff --git a/build-logic/convention/src/main/kotlin/AboutLibrariesConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AboutLibrariesConventionPlugin.kt
index 08cfb80826..4b8ba1177c 100644
--- a/build-logic/convention/src/main/kotlin/AboutLibrariesConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AboutLibrariesConventionPlugin.kt
@@ -66,7 +66,6 @@ class AboutLibrariesConventionPlugin : Plugin<Project> {
(it.name.endsWith("Resources") || it.name.endsWith("JavaRes"))
}
.configureEach { dependsOn("exportLibraryDefinitions") }
-
}
}
}
diff --git a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt
index ff902185b5..ee9ce4a526 100644
--- a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt
@@ -22,7 +22,6 @@ import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
-import org.meshtastic.buildlogic.isDesktopOnly
import org.meshtastic.buildlogic.library
import org.meshtastic.buildlogic.libs
@@ -50,9 +49,7 @@ class AndroidRoomConventionPlugin : Plugin<Project> {
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.getByName("commonMain").dependencies { implementation(roomRuntime) }
}
- if (!isDesktopOnly) {
- dependencies { add("kspAndroid", roomCompiler) }
- }
+ dependencies { add("kspAndroid", roomCompiler) }
dependencies { add("kspJvm", roomCompiler) }
}
diff --git a/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt
index a12edb1f94..5531c0d4dd 100644
--- a/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt
@@ -19,7 +19,6 @@ import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
-import org.meshtastic.buildlogic.isDesktopOnly
import org.meshtastic.buildlogic.library
import org.meshtastic.buildlogic.libs
@@ -61,14 +60,11 @@ class KmpFeatureConventionPlugin : Plugin<Project> {
implementation(libs.library("compose-multiplatform-ui-tooling-preview"))
}
- if (!isDesktopOnly) {
- sourceSets.getByName("androidMain").dependencies {
- // Common Android Compose dependencies
- implementation(libs.library("accompanist-permissions"))
- implementation(libs.library("androidx-activity-compose"))
+ sourceSets.getByName("androidMain").dependencies {
+ implementation(libs.library("accompanist-permissions"))
+ implementation(libs.library("androidx-activity-compose"))
- implementation(libs.library("compose-multiplatform-ui"))
- }
+ implementation(libs.library("compose-multiplatform-ui"))
}
sourceSets.getByName("commonTest").dependencies { implementation(project(":core:testing")) }
diff --git a/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt
index 6cc3eee95d..13ad495d9a 100644
--- a/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt
@@ -22,7 +22,6 @@ import org.meshtastic.buildlogic.configureGraphTasks
import org.meshtastic.buildlogic.configureKmpTestDependencies
import org.meshtastic.buildlogic.configureKotlinMultiplatform
import org.meshtastic.buildlogic.configureTestOptions
-import org.meshtastic.buildlogic.isDesktopOnly
import org.meshtastic.buildlogic.libs
import org.meshtastic.buildlogic.plugin
@@ -30,10 +29,8 @@ class KmpLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = libs.plugin("kotlin-multiplatform").get().pluginId)
- if (!isDesktopOnly) {
- apply(plugin = libs.plugin("android-kotlin-multiplatform-library").get().pluginId)
- apply(plugin = "meshtastic.android.lint")
- }
+ apply(plugin = libs.plugin("android-kotlin-multiplatform-library").get().pluginId)
+ apply(plugin = "meshtastic.android.lint")
apply(plugin = "meshtastic.detekt")
apply(plugin = "meshtastic.spotless")
apply(plugin = "meshtastic.dokka")
@@ -45,9 +42,7 @@ class KmpLibraryConventionPlugin : Plugin<Project> {
configureKmpTestDependencies()
configureTestOptions()
configureGraphTasks()
- if (!isDesktopOnly) {
- configureAndroidMarketplaceFallback()
- }
+ configureAndroidMarketplaceFallback()
}
}
}
diff --git a/build-logic/convention/src/main/kotlin/RootConventionPlugin.kt b/build-logic/convention/src/main/kotlin/RootConventionPlugin.kt
index 18549205e6..135306af6c 100644
--- a/build-logic/convention/src/main/kotlin/RootConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/RootConventionPlugin.kt
@@ -21,7 +21,6 @@ import org.meshtastic.buildlogic.configureDokkaAggregation
import org.meshtastic.buildlogic.configureGraphTasks
import org.meshtastic.buildlogic.configureKover
import org.meshtastic.buildlogic.configureKoverAggregation
-import org.meshtastic.buildlogic.isDesktopOnly
/**
* Root convention plugin applied to the top-level project.
@@ -66,9 +65,7 @@ private fun Project.registerKmpSmokeCompileTask() {
kmp.forEach { path ->
dependsOn("$path:compileKotlinJvm")
- if (!isDesktopOnly) {
- dependsOn("$path:compileKotlinIosSimulatorArm64")
- }
+ dependsOn("$path:compileKotlinIosSimulatorArm64")
}
}
}
@@ -76,7 +73,7 @@ private fun Project.registerKmpSmokeCompileTask() {
/** All modules included in `settings.gradle.kts`. Update this list when adding or removing modules. */
private val ALL_MODULES_FULL =
listOf(
- ":app",
+ ":androidApp",
":core:api",
":core:barcode",
":core:ble",
@@ -107,17 +104,16 @@ private val ALL_MODULES_FULL =
":feature:firmware",
":feature:wifi-provision",
":feature:widget",
- ":desktop",
+ ":desktopApp",
)
-/** Android-only modules excluded in desktop-only builds. */
-private val ANDROID_ONLY_MODULES = setOf(":app", ":core:api", ":core:barcode", ":feature:widget")
+/** Android-only modules that don't apply the KMP plugin. */
+private val ANDROID_ONLY_MODULES = setOf(":androidApp", ":core:api", ":core:barcode", ":feature:widget")
-private fun Project.allModules(): List<String> =
- if (isDesktopOnly) ALL_MODULES_FULL.filter { it !in ANDROID_ONLY_MODULES } else ALL_MODULES_FULL
+private fun allModules(): List<String> = ALL_MODULES_FULL
/**
- * Modules that apply the KMP plugin and should be compiled for JVM + iOS targets. Excludes pure-Android modules (:app,
- * :core:api, :core:barcode, :feature:widget) and the desktop JVM-only module.
+ * Modules that apply the KMP plugin and should be compiled for JVM + iOS targets. Excludes pure-Android modules
+ * (:androidApp, :core:api, :core:barcode, :feature:widget) and the desktop JVM-only module.
*/
-private fun Project.kmpModules(): List<String> = allModules().filter { it !in ANDROID_ONLY_MODULES + ":desktop" }
+private fun kmpModules(): List<String> = allModules().filter { it !in ANDROID_ONLY_MODULES + ":desktopApp" }
diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt
index cd294b657a..930a10413e 100644
--- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt
+++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt
@@ -137,7 +137,7 @@ fun Project.configureGraphTasks() {
pluginManager.hasPlugin("meshtastic.android.application.compose") ->
PluginType.AndroidApplication
- targetProjectPath.startsWith(":desktop") -> PluginType.ComposeDesktopApplication
+ targetProjectPath.startsWith(":desktopApp") -> PluginType.ComposeDesktopApplication
pluginManager.hasPlugin("meshtastic.kmp.feature") -> PluginType.KmpFeature
diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt
index abcae8595e..5229e42f1c 100644
--- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt
+++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt
@@ -81,48 +81,43 @@ internal fun Project.configureKotlinMultiplatform() {
// Standard KMP targets for Meshtastic
jvm()
- if (!isDesktopOnly) {
- // Configure the iOS targets for compile-only validation
- // We only add these for modules that already have KMP structure
- iosArm64()
- iosSimulatorArm64()
-
- // Configure the Android target if the plugin is applied
- pluginManager.withPlugin("com.android.kotlin.multiplatform.library") {
- extensions.findByType<KotlinMultiplatformAndroidLibraryTarget>()?.apply {
- compileSdk = configProperties.getProperty("COMPILE_SDK").toInt()
- minSdk = configProperties.getProperty("MIN_SDK").toInt()
-
- // Set the namespace automatically if not already set
- if (namespace == null) {
- val pkg = this@configureKotlinMultiplatform.path.removePrefix(":").replace(":", ".")
- namespace = "org.meshtastic.$pkg"
- }
+ // iOS targets for compile-only validation
+ iosArm64()
+ iosSimulatorArm64()
+
+ // Configure the Android target if the plugin is applied
+ pluginManager.withPlugin("com.android.kotlin.multiplatform.library") {
+ extensions.findByType<KotlinMultiplatformAndroidLibraryTarget>()?.apply {
+ compileSdk = configProperties.getProperty("COMPILE_SDK").toInt()
+ minSdk = configProperties.getProperty("MIN_SDK").toInt()
+
+ // Default: disable Android resources for most KMP modules.
+ // Modules that need resources (e.g. core:resources) override this
+ // explicitly in their build.gradle.kts androidLibrary {} block.
+ androidResources.enable = false
+
+ // Set the namespace automatically if not already set
+ if (namespace == null) {
+ val pkg = this@configureKotlinMultiplatform.path.removePrefix(":").replace(":", ".")
+ namespace = "org.meshtastic.$pkg"
}
}
- } else {
- // In desktop-only mode, create placeholder androidMain/iosMain source sets so
- // module build scripts that reference them via the DSL accessor don't fail.
- // These source sets are inert β no target compiles them.
- sourceSets.apply { create("androidMain") { dependsOn(getByName("commonMain")) } }
}
}
- if (!isDesktopOnly) {
- // Disable iOS native test link & run tasks.
- // iOS targets exist only for compile-time validation; linking test
- // executables is extremely slow and causes `./gradlew test` to hang.
- tasks.configureEach {
- val taskName = name.lowercase()
- if (taskName.contains("iosarm64") || taskName.contains("iossimulatorarm64")) {
- val isDisabledIosTask =
- (taskName.startsWith("link") && taskName.contains("test")) ||
- taskName == "iosarm64test" ||
- taskName == "iossimulatorarm64test" ||
- taskName.endsWith("testbinaries")
- if (isDisabledIosTask) {
- enabled = false
- }
+ // Disable iOS native test link & run tasks.
+ // iOS targets exist only for compile-time validation; linking test
+ // executables is extremely slow and causes `./gradlew test` to hang.
+ tasks.configureEach {
+ val taskName = name.lowercase()
+ if (taskName.contains("iosarm64") || taskName.contains("iossimulatorarm64")) {
+ val isDisabledIosTask =
+ (taskName.startsWith("link") && taskName.contains("test")) ||
+ taskName == "iosarm64test" ||
+ taskName == "iossimulatorarm64test" ||
+ taskName.endsWith("testbinaries")
+ if (isDisabledIosTask) {
+ enabled = false
}
}
}
@@ -275,9 +270,7 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
if (isPublishedModule) {
val toolchains = extensions.getByType(JavaToolchainService::class.java)
tasks.withType<Test>().configureEach {
- javaLauncher.set(
- toolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(APP_JDK)) }
- )
+ javaLauncher.set(toolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(APP_JDK)) })
}
}
}
diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt
index 05a775e0c7..3f2afaaf32 100644
--- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt
+++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt
@@ -36,17 +36,6 @@ import java.util.Properties
private const val MAX_TEST_RETRIES = 2
private const val MAX_TEST_FAILURES = 10
-/**
- * `true` when the build should only configure JVM (desktop) targets, skipping Android and iOS.
- *
- * Activate via environment variable (`DESKTOP_ONLY=true`) or Gradle property (`-Pdesktop.only=true`). This allows
- * building in environments without the Android SDK (e.g. Flatpak sandboxes).
- */
-val Project.isDesktopOnly: Boolean
- get() =
- providers.gradleProperty("desktop.only").orNull?.toBoolean() == true ||
- providers.environmentVariable("DESKTOP_ONLY").orNull?.toBoolean() == true
-
val Project.libs
get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
diff --git a/codecov.yml b/codecov.yml
index 7f77510ffe..0bccd30ce3 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -52,11 +52,11 @@ component_management:
- component_id: app
name: App
paths:
- - app/**
+ - androidApp/**
- component_id: desktop
name: Desktop
paths:
- - desktop/**
+ - desktopApp/**
ignore:
- "**/build/**"
diff --git a/core/barcode/README.md b/core/barcode/README.md
index c64fcca6c9..2fcbf8a05a 100644
--- a/core/barcode/README.md
+++ b/core/barcode/README.md
@@ -30,7 +30,7 @@ src/
## Usage
```kotlin
-// In a Composable (typically wired via LocalBarcodeScannerProvider in app/)
+// In a Composable (typically wired via LocalBarcodeScannerProvider in androidApp/)
val scanner = rememberBarcodeScanner { result ->
// Handle scanned QR code string (or null on dismiss)
}
diff --git a/core/ble/build.gradle.kts b/core/ble/build.gradle.kts
index ac6fe81255..337435c8e5 100644
--- a/core/ble/build.gradle.kts
+++ b/core/ble/build.gradle.kts
@@ -22,13 +22,7 @@ plugins {
}
kotlin {
- jvm()
-
- android {
- namespace = "org.meshtastic.core.ble"
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
+ androidLibrary { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts
index a1bc6261b5..11b33ca8f9 100644
--- a/core/common/build.gradle.kts
+++ b/core/common/build.gradle.kts
@@ -24,12 +24,7 @@ plugins {
}
kotlin {
- jvm()
-
- android {
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
+ androidLibrary { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
index d167e1ffef..6f35b5d5ff 100644
--- a/core/data/build.gradle.kts
+++ b/core/data/build.gradle.kts
@@ -23,13 +23,7 @@ plugins {
}
kotlin {
- jvm()
-
- android {
- namespace = "org.meshtastic.core.data"
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
+ androidLibrary { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts
index e732f5c782..b5a8617dcf 100644
--- a/core/database/build.gradle.kts
+++ b/core/database/build.gradle.kts
@@ -25,10 +25,7 @@ plugins {
}
kotlin {
- jvm()
-
- android {
- namespace = "org.meshtastic.core.database"
+ androidLibrary {
withHostTest { isIncludeAndroidResources = true }
withDeviceTest { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }
}
diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts
index cbf2edadb9..2f401fb847 100644
--- a/core/datastore/build.gradle.kts
+++ b/core/datastore/build.gradle.kts
@@ -22,13 +22,7 @@ plugins {
}
kotlin {
- jvm()
-
- android {
- namespace = "org.meshtastic.core.datastore"
- androidResources.enable = false
- withHostTest {}
- }
+ androidLibrary { withHostTest {} }
sourceSets {
commonMain.dependencies {
diff --git a/core/di/build.gradle.kts b/core/di/build.gradle.kts
index 6e0ada344e..75ce7456c7 100644
--- a/core/di/build.gradle.kts
+++ b/core/di/build.gradle.kts
@@ -21,13 +21,6 @@ plugins {
}
kotlin {
- jvm()
-
- android {
- namespace = "org.meshtastic.core.di"
- androidResources.enable = false
- }
-
sourceSets {
commonMain.dependencies {
implementation(projects.core.common)
diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts
index 71b9563972..2515332873 100644
--- a/core/domain/build.gradle.kts
+++ b/core/domain/build.gradle.kts
@@ -21,11 +21,7 @@ plugins {
}
kotlin {
- android {
- namespace = "org.meshtastic.core.domain"
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
+ androidLibrary { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts
index 7517bcef29..3c0dc95bd1 100644
--- a/core/model/build.gradle.kts
+++ b/core/model/build.gradle.kts
@@ -25,10 +25,7 @@ plugins {
}
kotlin {
- jvm()
-
- android {
- androidResources.enable = false
+ androidLibrary {
withHostTest { isIncludeAndroidResources = true }
withDeviceTest { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }
}
diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts
index bd7237a16d..633b86f8fa 100644
--- a/core/navigation/build.gradle.kts
+++ b/core/navigation/build.gradle.kts
@@ -23,8 +23,6 @@ plugins {
}
kotlin {
- android { namespace = "org.meshtastic.core.navigation" }
-
sourceSets {
commonMain.dependencies {
implementation(projects.core.common)
diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts
index d71a1d1af0..4d0380ac65 100644
--- a/core/network/build.gradle.kts
+++ b/core/network/build.gradle.kts
@@ -23,11 +23,7 @@ plugins {
}
kotlin {
- android {
- namespace = "org.meshtastic.core.network"
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
+ androidLibrary { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
diff --git a/core/nfc/build.gradle.kts b/core/nfc/build.gradle.kts
index 355e38d081..935eec87b1 100644
--- a/core/nfc/build.gradle.kts
+++ b/core/nfc/build.gradle.kts
@@ -21,11 +21,6 @@ plugins {
}
kotlin {
- android {
- namespace = "org.meshtastic.core.nfc"
- androidResources.enable = false
- }
-
sourceSets {
commonMain.dependencies { implementation(libs.kermit) }
diff --git a/core/prefs/build.gradle.kts b/core/prefs/build.gradle.kts
index a37d65e484..f423ca9389 100644
--- a/core/prefs/build.gradle.kts
+++ b/core/prefs/build.gradle.kts
@@ -21,11 +21,7 @@ plugins {
}
kotlin {
- android {
- namespace = "org.meshtastic.core.prefs"
- androidResources.enable = false
- withHostTest {}
- }
+ androidLibrary { withHostTest {} }
sourceSets {
commonMain.dependencies {
diff --git a/core/proto/build.gradle.kts b/core/proto/build.gradle.kts
index 112ecf936f..683514a3c8 100644
--- a/core/proto/build.gradle.kts
+++ b/core/proto/build.gradle.kts
@@ -24,7 +24,7 @@ plugins {
kotlin {
// Override minSdk for ATAK compatibility (standard is 26)
- android { minSdk = 21 }
+ androidLibrary { minSdk = 21 }
sourceSets {
commonMain.dependencies {
diff --git a/core/repository/build.gradle.kts b/core/repository/build.gradle.kts
index b090517008..6d1185c41b 100644
--- a/core/repository/build.gradle.kts
+++ b/core/repository/build.gradle.kts
@@ -21,10 +21,7 @@ plugins {
}
kotlin {
- android {
- androidResources.enable = false
- withHostTest {}
- }
+ androidLibrary { withHostTest {} }
sourceSets {
commonMain.dependencies {
diff --git a/core/resources/build.gradle.kts b/core/resources/build.gradle.kts
index e8b185dde3..82fe2a669e 100644
--- a/core/resources/build.gradle.kts
+++ b/core/resources/build.gradle.kts
@@ -21,9 +21,7 @@ plugins {
}
kotlin {
- jvm()
-
- android {
+ androidLibrary {
androidResources {
enable = true
resourcePrefix = "meshtastic_"
diff --git a/core/service/build.gradle.kts b/core/service/build.gradle.kts
index 65c10432dc..59f7d3f959 100644
--- a/core/service/build.gradle.kts
+++ b/core/service/build.gradle.kts
@@ -21,11 +21,7 @@ plugins {
}
kotlin {
- android {
- namespace = "org.meshtastic.core.service"
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
+ androidLibrary { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
diff --git a/core/takserver/build.gradle.kts b/core/takserver/build.gradle.kts
index c9e69c9e83..2aa9d2ec60 100644
--- a/core/takserver/build.gradle.kts
+++ b/core/takserver/build.gradle.kts
@@ -24,13 +24,7 @@ plugins {
kotlin {
@Suppress("UnstableApiUsage")
- android {
- namespace = "org.meshtastic.core.takserver"
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
-
- jvm {}
+ androidLibrary { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
diff --git a/core/testing/build.gradle.kts b/core/testing/build.gradle.kts
index 7f5f000f1c..315a8ddb24 100644
--- a/core/testing/build.gradle.kts
+++ b/core/testing/build.gradle.kts
@@ -18,11 +18,7 @@
plugins { alias(libs.plugins.meshtastic.kmp.library) }
kotlin {
- android {
- namespace = "org.meshtastic.core.testing"
- androidResources.enable = false
- withHostTest {}
- }
+ androidLibrary { withHostTest {} }
sourceSets {
commonMain.dependencies {
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index d2779769b2..5453ea2f23 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -23,11 +23,6 @@ plugins {
}
kotlin {
- android {
- namespace = "org.meshtastic.core.ui"
- androidResources.enable = false
- }
-
sourceSets {
commonMain.dependencies {
implementation(projects.core.common)
diff --git a/desktop/.gitignore b/desktopApp/.gitignore
similarity index 100%
rename from desktop/.gitignore
rename to desktopApp/.gitignore
diff --git a/desktop/README.md b/desktopApp/README.md
similarity index 93%
rename from desktop/README.md
rename to desktopApp/README.md
index 7a9daf9c89..cd6383b0c7 100644
--- a/desktop/README.md
+++ b/desktopApp/README.md
@@ -1,4 +1,4 @@
-# `:desktop` β Meshtastic Desktop
+# `:desktopApp` β Meshtastic Desktop
A Compose Desktop application target β the first full non-Android target for the shared KMP module graph. This module serves as:
@@ -11,21 +11,21 @@ A Compose Desktop application target β the first full non-Android target for t
```bash
# Run the desktop app
-./gradlew :desktop:run
+./gradlew :desktopApp:run
# Run tests
-./gradlew :desktop:test
+./gradlew :desktopApp:test
# Package native distribution (DMG/MSI/DEB) β debug (no ProGuard)
-./gradlew :desktop:packageDistributionForCurrentOS
+./gradlew :desktopApp:packageDistributionForCurrentOS
# Package native distribution (DMG/MSI/DEB) β release (ProGuard minified)
-./gradlew :desktop:packageReleaseDistributionForCurrentOS
+./gradlew :desktopApp:packageReleaseDistributionForCurrentOS
```
## ProGuard / Minification
-Release builds use ProGuard for tree-shaking (unused code removal), significantly reducing distribution size. Obfuscation is disabled since the project is open-source. Rules are aligned with the Android R8 rules in `app/proguard-rules.pro` β both targets share the same anti-class-merging philosophy.
+Release builds use ProGuard for tree-shaking (unused code removal), significantly reducing distribution size. Obfuscation is disabled since the project is open-source. Rules are aligned with the Android R8 rules in `androidApp/proguard-rules.pro` β both targets share the same anti-class-merging philosophy.
**Configuration:**
- `build.gradle.kts` β `buildTypes.release.proguard` block enables ProGuard with `optimize.set(true)` and `obfuscate.set(false)`.
@@ -36,10 +36,10 @@ Release builds use ProGuard for tree-shaking (unused code removal), significantl
- **Room KMP** β Uses `androidx.room3` package path (Room KMP 3.x).
**Troubleshooting ProGuard issues:**
-- If the release build crashes at runtime with `ClassNotFoundException` or `NoSuchMethodError`, a library is loading classes via reflection that ProGuard stripped. Add a `-keep` rule in `proguard-rules.pro` **and** the corresponding rule in `app/proguard-rules.pro` to keep both targets aligned.
-- To debug which classes ProGuard removes, temporarily add `-printusage proguard-usage.txt` to the rules file and inspect the output in `desktop/proguard-usage.txt`.
+- If the release build crashes at runtime with `ClassNotFoundException` or `NoSuchMethodError`, a library is loading classes via reflection that ProGuard stripped. Add a `-keep` rule in `proguard-rules.pro` **and** the corresponding rule in `androidApp/proguard-rules.pro` to keep both targets aligned.
+- To debug which classes ProGuard removes, temporarily add `-printusage proguard-usage.txt` to the rules file and inspect the output in `desktopApp/proguard-usage.txt`.
- To see the full mapping of optimizations applied, add `-printseeds proguard-seeds.txt`.
-- Run `./gradlew :desktop:runRelease` for a quick smoke-test of the minified app before packaging.
+- Run `./gradlew :desktopApp:runRelease` for a quick smoke-test of the minified app before packaging.
## Architecture
diff --git a/desktop/build.gradle.kts b/desktopApp/build.gradle.kts
similarity index 100%
rename from desktop/build.gradle.kts
rename to desktopApp/build.gradle.kts
diff --git a/desktop/detekt-baseline.xml b/desktopApp/detekt-baseline.xml
similarity index 100%
rename from desktop/detekt-baseline.xml
rename to desktopApp/detekt-baseline.xml
diff --git a/desktop/entitlements.plist b/desktopApp/entitlements.plist
similarity index 100%
rename from desktop/entitlements.plist
rename to desktopApp/entitlements.plist
diff --git a/desktop/proguard-rules.pro b/desktopApp/proguard-rules.pro
similarity index 100%
rename from desktop/proguard-rules.pro
rename to desktopApp/proguard-rules.pro
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/data/DesktopPreferencesDataSource.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/data/DesktopPreferencesDataSource.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/data/DesktopPreferencesDataSource.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/data/DesktopPreferencesDataSource.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopDiModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopDiModule.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopDiModule.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopDiModule.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopOS.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopOS.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopOS.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopOS.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/NativeNotificationSender.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/NativeNotificationSender.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/notification/NativeNotificationSender.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/NativeNotificationSender.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSender.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSender.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSender.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSender.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/stub/CompassStubs.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/CompassStubs.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/stub/CompassStubs.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/CompassStubs.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt
diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt
similarity index 100%
rename from desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt
rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt
diff --git a/desktop/src/main/resources/icon.icns b/desktopApp/src/main/resources/icon.icns
similarity index 100%
rename from desktop/src/main/resources/icon.icns
rename to desktopApp/src/main/resources/icon.icns
diff --git a/desktop/src/main/resources/icon.ico b/desktopApp/src/main/resources/icon.ico
similarity index 100%
rename from desktop/src/main/resources/icon.ico
rename to desktopApp/src/main/resources/icon.ico
diff --git a/desktop/src/main/resources/icon.png b/desktopApp/src/main/resources/icon.png
similarity index 100%
rename from desktop/src/main/resources/icon.png
rename to desktopApp/src/main/resources/icon.png
diff --git a/desktop/src/main/resources/tray_icon_black.svg b/desktopApp/src/main/resources/tray_icon_black.svg
similarity index 100%
rename from desktop/src/main/resources/tray_icon_black.svg
rename to desktopApp/src/main/resources/tray_icon_black.svg
diff --git a/desktop/src/main/resources/tray_icon_white.svg b/desktopApp/src/main/resources/tray_icon_white.svg
similarity index 100%
rename from desktop/src/main/resources/tray_icon_white.svg
rename to desktopApp/src/main/resources/tray_icon_white.svg
diff --git a/desktop/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt
similarity index 100%
rename from desktop/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt
rename to desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt
diff --git a/desktop/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt
similarity index 100%
rename from desktop/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt
rename to desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt
diff --git a/desktop/src/test/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSenderTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSenderTest.kt
similarity index 100%
rename from desktop/src/test/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSenderTest.kt
rename to desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSenderTest.kt
diff --git a/desktop/src/test/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSenderTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSenderTest.kt
similarity index 100%
rename from desktop/src/test/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSenderTest.kt
rename to desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSenderTest.kt
diff --git a/desktop/src/test/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSenderTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSenderTest.kt
similarity index 100%
rename from desktop/src/test/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSenderTest.kt
rename to desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSenderTest.kt
diff --git a/desktop/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt
similarity index 100%
rename from desktop/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt
rename to desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt
diff --git a/feature/connections/build.gradle.kts b/feature/connections/build.gradle.kts
index 18d7673a94..d6160c1a2f 100644
--- a/feature/connections/build.gradle.kts
+++ b/feature/connections/build.gradle.kts
@@ -18,11 +18,7 @@
plugins { alias(libs.plugins.meshtastic.kmp.feature) }
kotlin {
- android {
- namespace = "org.meshtastic.feature.connections"
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
+ androidLibrary { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts
index 9c83bee0e8..8cdd054ec0 100644
--- a/feature/firmware/build.gradle.kts
+++ b/feature/firmware/build.gradle.kts
@@ -21,11 +21,7 @@ plugins {
}
kotlin {
- android {
- namespace = "org.meshtastic.feature.firmware"
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
+ androidLibrary { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
diff --git a/feature/intro/build.gradle.kts b/feature/intro/build.gradle.kts
index a55154d6a8..0d615ab025 100644
--- a/feature/intro/build.gradle.kts
+++ b/feature/intro/build.gradle.kts
@@ -21,11 +21,7 @@ plugins {
}
kotlin {
- android {
- namespace = "org.meshtastic.feature.intro"
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
+ androidLibrary { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
diff --git a/feature/map/README.md b/feature/map/README.md
index 802f18913c..8e393212cc 100644
--- a/feature/map/README.md
+++ b/feature/map/README.md
@@ -1,7 +1,7 @@
# `:feature:map`
## Overview
-The `:feature:map` module provides the mapping interface for the application. Map rendering is decomposed into three focused `CompositionLocal` provider contracts, each with per-flavor implementations in `:app`.
+The `:feature:map` module provides the mapping interface for the application. Map rendering is decomposed into three focused `CompositionLocal` provider contracts, each with per-flavor implementations in `:androidApp`.
## Architecture
@@ -28,8 +28,8 @@ All providers are injected via `CompositionLocal` in `MainActivity.kt` and consu
## Map Providers
-- **Google Maps (`google` flavor)**: Uses Google Play Services Maps SDK. Implementations in `app/src/google/kotlin/org/meshtastic/app/map/`.
-- **OpenStreetMap (`fdroid` flavor)**: Uses `osmdroid` for a fully open-source experience. Implementations in `app/src/fdroid/kotlin/org/meshtastic/app/map/`.
+- **Google Maps (`google` flavor)**: Uses Google Play Services Maps SDK. Implementations in `androidApp/src/google/kotlin/org/meshtastic/app/map/`.
+- **OpenStreetMap (`fdroid` flavor)**: Uses `osmdroid` for a fully open-source experience. Implementations in `androidApp/src/fdroid/kotlin/org/meshtastic/app/map/`.
## Features
- **Live Node Tracking**: Real-time position updates for nodes on the mesh.
diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts
index 42506ea334..3f0f3d11cf 100644
--- a/feature/map/build.gradle.kts
+++ b/feature/map/build.gradle.kts
@@ -20,11 +20,7 @@ plugins {
}
kotlin {
- android {
- namespace = "org.meshtastic.feature.map"
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
+ androidLibrary { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts
index 3a1c79a1a1..39bcfb4614 100644
--- a/feature/messaging/build.gradle.kts
+++ b/feature/messaging/build.gradle.kts
@@ -21,11 +21,6 @@ plugins {
}
kotlin {
- android {
- namespace = "org.meshtastic.feature.messaging"
- androidResources.enable = false
- }
-
sourceSets {
commonMain.dependencies {
implementation(libs.compose.multiplatform.foundation)
@@ -61,11 +56,11 @@ kotlin {
}
// Gradle's KMP variant resolution follows `available-at` redirects in module
-// metadata and needs android variant `.module` files for disambiguation, even
-// when building desktop-only offline. The androidCompileClasspath configuration
-// can't be resolved by the flatpak generator due to AGP variant ambiguity on
-// project dependencies, so we capture android KMP metadata via a dedicated
-// configuration that only holds external dependencies.
+// metadata and needs android variant `.module` files for disambiguation. The
+// androidCompileClasspath configuration can't be resolved by the flatpak
+// generator due to AGP variant ambiguity on project dependencies, so we capture
+// android KMP metadata via a dedicated configuration that only holds external
+// dependencies.
val flatpakKmpAndroidMeta by
configurations.creating {
isCanBeResolved = true
diff --git a/feature/node/build.gradle.kts b/feature/node/build.gradle.kts
index c909082806..51f0ae758e 100644
--- a/feature/node/build.gradle.kts
+++ b/feature/node/build.gradle.kts
@@ -21,11 +21,7 @@ plugins {
}
kotlin {
- android {
- namespace = "org.meshtastic.feature.node"
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
+ androidLibrary { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts
index 3725dd2933..3ef213edd5 100644
--- a/feature/settings/build.gradle.kts
+++ b/feature/settings/build.gradle.kts
@@ -22,11 +22,6 @@ plugins {
}
kotlin {
- android {
- namespace = "org.meshtastic.feature.settings"
- androidResources.enable = false
- }
-
sourceSets {
commonMain.dependencies {
implementation(projects.core.common)
diff --git a/feature/wifi-provision/build.gradle.kts b/feature/wifi-provision/build.gradle.kts
index ca1498b1e9..ec0b4a0fe9 100644
--- a/feature/wifi-provision/build.gradle.kts
+++ b/feature/wifi-provision/build.gradle.kts
@@ -20,9 +20,8 @@ plugins {
}
kotlin {
- android {
+ androidLibrary {
namespace = "org.meshtastic.feature.wifiprovision"
- androidResources.enable = false
withHostTest {}
}
diff --git a/scripts/desktop-only-prep.sh b/scripts/desktop-only-prep.sh
deleted file mode 100755
index 0bd0c41896..0000000000
--- a/scripts/desktop-only-prep.sh
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env bash
-# desktop-only-prep.sh β Prepare source tree for a desktop-only (JVM) build.
-#
-# Usage:
-# ./scripts/desktop-only-prep.sh
-# DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS
-#
-# This script comments out Android-specific blocks in module build scripts so
-# Gradle can configure without the Android SDK. It is designed for Flatpak and
-# other sandboxed Linux packaging environments.
-#
-# Prerequisites:
-# npm install -g @ast-grep/cli
-# OR
-# pipx install ast-grep-cli
-#
-# The companion in-code guards in build-logic convention plugins handle:
-# - Skipping Android/iOS plugin application (KmpLibraryConventionPlugin)
-# - Skipping iOS targets (configureKotlinMultiplatform)
-# - Creating a placeholder androidMain source set
-# - Excluding Android-only modules from settings.gradle.kts
-#
-# This script handles what can't be done in-code:
-# - `kotlin { android { ... } }` blocks in module build.gradle.kts files
-# - `androidMain.dependencies { ... }` blocks with project dependencies to
-# excluded modules (e.g., projects.core.barcode, projects.core.api)
-#
-# To reverse: `git checkout -- .` or rebuild from clean source.
-
-set -euo pipefail
-
-REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
-SCRIPT_DIR="$REPO_ROOT/scripts"
-cd "$REPO_ROOT"
-
-if ! command -v sg &>/dev/null; then
- echo "ERROR: ast-grep (sg) is required but not found in PATH." >&2
- echo " Install: https://ast-grep.github.io/guide/quick-start.html" >&2
- exit 1
-fi
-
-echo "==> Preparing desktop-only build..."
-
-# Collect target build.gradle.kts files (excluding modules that stay as-is)
-mapfile -t FILES < <(find . -name "build.gradle.kts" \
- -not -path "./build-logic/*" \
- -not -path "./.agent_refs/*" \
- -not -path "./coil/*" \
- -not -path "./kable/*" \
- -not -path "./app/*" \
- -not -path "./core/api/*" \
- -not -path "./core/barcode/*" \
- -not -path "./feature/widget/*" \
- -not -path "./desktop/*" \
- -not -path "./build/*")
-
-if [[ ${#FILES[@]} -eq 0 ]]; then
- echo "WARNING: No build.gradle.kts files found to process." >&2
- exit 0
-fi
-
-# Apply all ast-grep rules from the rules directory.
-# Each rule YAML defines a pattern to match and a fix (comment replacement).
-sg scan -c "$SCRIPT_DIR/sgconfig.yml" --update-all "${FILES[@]}"
-
-echo "==> Desktop-only prep complete."
-echo " Run: DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS"
diff --git a/scripts/desktop-only-rules/remove-android-block.yml b/scripts/desktop-only-rules/remove-android-block.yml
deleted file mode 100644
index 972b63e3f7..0000000000
--- a/scripts/desktop-only-rules/remove-android-block.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-id: remove-android-block
-language: kotlin
-severity: warning
-message: "Remove android { } block for desktop-only build"
-rule:
- pattern: |-
- android {
- $$$BODY
- }
-fix: "// [desktop-only] android { ... } block removed"
diff --git a/scripts/desktop-only-rules/remove-android-device-test.yml b/scripts/desktop-only-rules/remove-android-device-test.yml
deleted file mode 100644
index 4d653681be..0000000000
--- a/scripts/desktop-only-rules/remove-android-device-test.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-id: remove-android-device-test
-language: kotlin
-severity: warning
-message: "Remove val androidDeviceTest block for desktop-only build"
-rule:
- pattern: |-
- val androidDeviceTest by getting {
- $$$BODY
- }
-fix: "// [desktop-only] val androidDeviceTest { ... } block removed"
diff --git a/scripts/desktop-only-rules/remove-android-host-test.yml b/scripts/desktop-only-rules/remove-android-host-test.yml
deleted file mode 100644
index 0d68e32b36..0000000000
--- a/scripts/desktop-only-rules/remove-android-host-test.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-id: remove-android-host-test
-language: kotlin
-severity: warning
-message: "Remove val androidHostTest block for desktop-only build"
-rule:
- pattern: |-
- val androidHostTest by getting {
- $$$BODY
- }
-fix: "// [desktop-only] val androidHostTest { ... } block removed"
diff --git a/scripts/desktop-only-rules/remove-android-imports.yml b/scripts/desktop-only-rules/remove-android-imports.yml
deleted file mode 100644
index 35060f4556..0000000000
--- a/scripts/desktop-only-rules/remove-android-imports.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-id: remove-android-imports
-language: kotlin
-severity: warning
-message: "Remove com.android import for desktop-only build"
-rule:
- pattern: "import com.android.$$$REST"
-fix: "// [desktop-only] android import removed"
diff --git a/scripts/desktop-only-rules/remove-android-instrumented-test.yml b/scripts/desktop-only-rules/remove-android-instrumented-test.yml
deleted file mode 100644
index 4091c617ef..0000000000
--- a/scripts/desktop-only-rules/remove-android-instrumented-test.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-id: remove-android-instrumented-test
-language: kotlin
-severity: warning
-message: "Remove val androidInstrumentedTest block for desktop-only build"
-rule:
- pattern: |-
- val androidInstrumentedTest by getting {
- $$$BODY
- }
-fix: "// [desktop-only] val androidInstrumentedTest { ... } block removed"
diff --git a/scripts/desktop-only-rules/remove-android-main-deps.yml b/scripts/desktop-only-rules/remove-android-main-deps.yml
deleted file mode 100644
index 3dd1cd72d7..0000000000
--- a/scripts/desktop-only-rules/remove-android-main-deps.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-id: remove-android-main-deps
-language: kotlin
-severity: warning
-message: "Remove androidMain.dependencies { } block for desktop-only build"
-rule:
- pattern: |-
- androidMain.dependencies {
- $$$BODY
- }
-fix: "// [desktop-only] androidMain.dependencies { ... } block removed"
diff --git a/scripts/desktop-only-rules/remove-ksp-android.yml b/scripts/desktop-only-rules/remove-ksp-android.yml
deleted file mode 100644
index 7ea8680c84..0000000000
--- a/scripts/desktop-only-rules/remove-ksp-android.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-id: remove-ksp-android
-language: kotlin
-severity: warning
-message: "Remove kspAndroid configuration for desktop-only build"
-rule:
- kind: call_expression
- regex: "^\"kspAndroid"
-fix: "// [desktop-only] kspAndroid config removed"
diff --git a/scripts/desktop-only-rules/remove-parcelize-plugin.yml b/scripts/desktop-only-rules/remove-parcelize-plugin.yml
deleted file mode 100644
index 9dd969cdab..0000000000
--- a/scripts/desktop-only-rules/remove-parcelize-plugin.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-id: remove-parcelize-plugin
-language: kotlin
-severity: warning
-message: "Remove parcelize plugin for desktop-only build"
-rule:
- pattern: "alias(libs.plugins.kotlin.parcelize)"
-fix: "// [desktop-only] alias(libs.plugins.kotlin.parcelize)"
diff --git a/scripts/sgconfig.yml b/scripts/sgconfig.yml
deleted file mode 100644
index 0366edac1b..0000000000
--- a/scripts/sgconfig.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-ruleDirs:
- - desktop-only-rules
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 4b41e89369..6d37b65590 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -25,12 +25,6 @@ pluginManagement {
}
}
-// Desktop-only mode: skip Android-only modules when ANDROID_HOME is unavailable (e.g. Flatpak builds).
-// Activate via: DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS
-val desktopOnly =
- providers.gradleProperty("desktop.only").orNull?.toBoolean() == true ||
- System.getenv("DESKTOP_ONLY")?.toBoolean() == true
-
plugins {
id("org.gradle.toolchains.foojay-resolver") version "1.0.0"
id("com.gradle.develocity") version "4.4.1"
@@ -112,15 +106,10 @@ include(
":feature:settings",
":feature:firmware",
":feature:wifi-provision",
- ":desktop",
+ ":desktopApp",
+ ":androidApp",
+ ":core:api",
+ ":core:barcode",
+ ":feature:widget",
+ ":screenshot-tests",
)
-
-if (!desktopOnly) {
- include(
- ":app",
- ":core:api",
- ":core:barcode",
- ":feature:widget",
- ":screenshot-tests",
- )
-}
diff --git a/specs/006-kmp-project-structure/checklists/build-migration.md b/specs/006-kmp-project-structure/checklists/build-migration.md
new file mode 100644
index 0000000000..20fa71b60f
--- /dev/null
+++ b/specs/006-kmp-project-structure/checklists/build-migration.md
@@ -0,0 +1,110 @@
+# Build Migration Checklist: KMP Recommended Project Structure Alignment
+
+**Purpose**: Validate the quality, completeness, and clarity of requirements for migrating 27 KMP modules from legacy `android {}` blocks to `kotlin.androidLibrary {}` DSL, hardening convention plugins, and verifying module boundaries.
+**Created**: 2025-07-16
+**Feature**: [spec.md](../spec.md) | [plan.md](../plan.md) | [tasks.md](../tasks.md)
+**Focus**: Build infrastructure migration β DSL correctness, convention plugin hardening, build verification
+**Audience**: PR Reviewer
+**Depth**: Standard
+
+---
+
+## Requirement Completeness
+
+- [ ] CHK001 β Are migration requirements specified for all 27 KMP modules individually, with per-module expected outcome documented? [Completeness, Spec Β§FR-001, Data Model Β§Migration Matrix]
+- [ ] CHK002 β Are the 4 excluded modules (`feature:widget`, `core:api`, `core:barcode`, `screenshot-tests`) explicitly listed with justification for exclusion? [Completeness, Spec Β§FR-004]
+- [ ] CHK003 β Is the convention plugin enhancement requirement (`androidResources.enable = false` default) specified with exact insertion point and backward-compatibility rationale? [Completeness, Spec Β§FR-003, Β§FR-007]
+- [ ] CHK004 β Are build verification requirements defined for every migration phase, not just the final state? [Completeness, Plan Β§Phase Dependencies]
+- [ ] CHK005 β Are requirements defined for what constitutes a "clean build baseline" measurement in Phase 1? [Completeness, Gap β T001 mentions "record timings" but format/storage unspecified]
+- [ ] CHK006 β Is the redundant `jvm()` cleanup requirement documented with the full list of affected modules (14 modules per data-model.md)? [Completeness, Data Model Β§Redundant jvm() cleanup]
+- [ ] CHK007 β Are requirements specified for updating `quickstart.md` if migration steps differ from what was actually executed? [Completeness, Tasks Β§T047]
+
+## Requirement Clarity
+
+- [x] CHK008 β Is the distinction between "28 KMP modules" (spec Β§Summary) and "27 KMP modules" (plan, data-model) resolved with an authoritative count? [Clarity, Ambiguity β β
Fixed: all references now say 27]
+- [ ] CHK009 β Is "legacy `android {}` blocks inside `kotlin {}`" precisely defined to distinguish from `android {}` in non-KMP modules and from `androidLibrary {}` blocks? [Clarity, Spec Β§FR-001]
+- [ ] CHK010 β Is the NFR-001 build-time threshold ("not increase by more than 5%") specified with measurement methodology β cold vs warm build, CI vs local, which tasks? [Clarity, Spec Β§NFR-001]
+- [ ] CHK011 β Is "auto-derived namespace" behavior documented with the exact derivation formula (`:core:di` β `org.meshtastic.core.di`) so reviewers can verify correctness? [Clarity, Quickstart Β§Step 1]
+- [ ] CHK012 β Is the `feature:wifi-provision` namespace mismatch clearly documented β why auto-derived `feature.wifi.provision` differs from required `feature.wifiprovision` and that this is the only override? [Clarity, Tasks Β§T030, Data Model Β§Tier 2c]
+- [ ] CHK013 β Is "uses only `KotlinMultiplatformAndroidLibraryTarget` API" (SC-007) defined with enough specificity to distinguish compliant from non-compliant patterns in the convention plugin? [Clarity, Spec Β§SC-007]
+- [ ] CHK014 β Is the `core:proto` minSdk override requirement (`minSdk = 21`) specified with rationale ("ATAK compatibility") that a reviewer can validate as still necessary? [Clarity, Tasks Β§T032]
+
+## Requirement Consistency
+
+- [ ] CHK015 β Are the tier classifications consistent between data-model.md, tasks.md, and quickstart.md for all 27 modules? [Consistency, Cross-Artifact]
+- [ ] CHK016 β Does the `core:model` module appear in exactly one tier across all artifacts (Tier 2a in data-model.md, not duplicated in Tier 1)? [Consistency, Plan Β§Notes β explicit callout of double-count]
+- [ ] CHK017 β Are the `withHostTest` configurations (empty `{}` vs `{ isIncludeAndroidResources = true }`) consistent between data-model.md and tasks.md for all 18 Tier 2 modules? [Consistency]
+- [ ] CHK018 β Do quickstart.md code examples match the exact DSL patterns specified in data-model.md "After migration" column? [Consistency, Cross-Artifact]
+- [ ] CHK019 β Are convention plugin file paths consistent across spec.md (Β§Key Components), plan.md (Β§Source Code), and tasks.md (T004, T043)? [Consistency]
+- [ ] CHK020 β Is the `core:resources` module consistently documented as the sole module requiring `androidResources.enable = true` across all artifacts? [Consistency, Data Model Β§Validation Rules]
+
+## Acceptance Criteria Quality
+
+- [ ] CHK021 β Are success criteria SC-001 through SC-007 all objectively measurable with specific commands or grep patterns? [Measurability, Spec Β§Success Criteria]
+- [ ] CHK022 β Is SC-006 ("new contributor can add a new KMP module") testable as-stated, or does it need a concrete verification procedure? [Measurability, Spec Β§SC-006 β no test procedure defined]
+- [ ] CHK023 β Is SC-005 ("build time within 5%") measurable given that build times vary by machine β are multiple-run averaging or CI-based measurements specified? [Measurability, Spec Β§SC-005]
+- [ ] CHK024 β Are the grep validation commands in tasks.md (T041, T042) specified with sufficient exclusion patterns to avoid false positives from Android-only modules and unrelated string matches? [Measurability, Tasks Β§T041]
+- [ ] CHK025 β Does each user story's "Independent Test" section provide a concrete, copy-pasteable verification command? [Measurability, Spec Β§US1βUS4]
+
+## Scenario Coverage
+
+- [ ] CHK026 β Are requirements defined for what happens if a single module migration fails mid-batch β can other modules in the same tier proceed? [Coverage, Exception Flow, Gap]
+- [x] CHK027 β Are rollback requirements specified at per-module, per-tier, and full-migration granularity? [Coverage β β
Fixed: quickstart.md Rollback Plan now includes per-module, per-tier, and full-migration commands]
+- [ ] CHK028 β Are requirements specified for the `DESKTOP_ONLY=true` build mode after each migration phase, or only at final verification? [Coverage, Spec Β§FR-005 β only tested in Phase 6 T039]
+- [ ] CHK029 β Are CI/CD pipeline implications documented β will the migration PR pass existing CI checks without CI configuration changes? [Coverage, Gap]
+- [ ] CHK030 β Are requirements specified for what happens if Gradle configuration cache is invalidated by the DSL migration? [Coverage, Spec Β§NFR-002]
+
+## Edge Case Coverage
+
+- [ ] CHK031 β Is the `core:database` module's `withDeviceTest` + `withHostTest` combination requirement fully specified, including the exact `instrumentationRunner` value? [Edge Case, Data Model Β§Tier 3]
+- [ ] CHK032 β Is the `core:resources` module's resource prefix (`meshtastic_`) override requirement specified alongside the `androidResources.enable = true` override? [Edge Case, Data Model Β§Tier 3]
+- [ ] CHK033 β Are requirements defined for modules that use the `meshtastic.kmp.jvm.android` plugin β does the DSL migration affect the `jvmAndroidMain` shared source set? [Edge Case, Spec Β§Assumptions]
+- [ ] CHK034 β Is the behavior specified when a module's auto-derived namespace collides with another module's namespace? [Edge Case, Gap]
+- [ ] CHK035 β Are requirements specified for the convention plugin's behavior when `namespace` is already set by a module (explicit override takes precedence)? [Edge Case, Quickstart Β§Step 1 β "if null" guard]
+
+## Non-Functional Requirements
+
+- [ ] CHK036 β Is NFR-003 ("no intermediate broken states on main branch") achievable as specified β does the plan ensure atomic commit boundaries? [NFR, Spec Β§NFR-003]
+- [ ] CHK037 β Is NFR-004 ("documentable as step-by-step checklist for other repositories") satisfied by quickstart.md, or is additional documentation needed? [NFR, Spec Β§NFR-004]
+- [ ] CHK038 β Are Gradle compatibility requirements specified β minimum Gradle version, configuration cache compatibility, isolated projects mode? [NFR, Spec Β§NFR-002]
+- [ ] CHK039 β Are formatting and static analysis requirements (spotlessCheck, detekt) specified as part of every phase verification, or only in Phase 7? [NFR, Tasks β only T045 in Phase 7]
+
+## Dependencies & Assumptions
+
+- [ ] CHK040 β Is the assumption "project already runs AGP 9.2.1" validated with a specific version check requirement? [Assumption, Spec Β§Assumptions]
+- [ ] CHK041 β Is the assumption "no module uses deprecated `androidTarget {}` call" specified as a pre-migration verification step? [Assumption, Spec Β§Assumptions]
+- [ ] CHK042 β Is the Phase 2 β Phase 3/4/5 blocking dependency clearly specified β convention plugin hardening MUST complete before any module migration? [Dependency, Plan Β§Phase Dependencies]
+- [ ] CHK043 β Is the `jvmAndroidMain` shared source set compatibility assumption validated β are there requirements to verify this pattern works with the new DSL? [Assumption, Spec Β§Assumptions]
+- [ ] CHK044 β Is the backward-compatibility assumption for T004 (adding `androidResources.enable = false` default) documented with rationale that existing explicit overrides become no-ops? [Assumption, Plan Β§Key Risk Mitigation]
+
+## Ambiguities & Conflicts
+
+- [x] CHK045 β Is the "28 vs 27 modules" discrepancy resolved authoritatively in spec.md, or does the spec still say 28 while plan says 27? [Conflict β β
Fixed: all 5 instances of "28" updated to "27" in spec.md]
+- [x] CHK046 β Does FR-008 ("prevent future modules from using legacy pattern") have a concrete enforcement mechanism specified, or is it aspirational? [Ambiguity β β
Fixed: T048 added to tasks.md β CI grep check or Gradle afterEvaluate assertion]
+- [ ] CHK047 β Is it clear whether the `kotlin.androidLibrary {}` block should be removed entirely for Tier 1 modules or left as an empty block? [Ambiguity, Data Model says "removed", Quickstart shows no block]
+- [ ] CHK048 β Is the scope of "convention plugin hardening" bounded β does FR-007 ("audit and update") have a defined completion criterion beyond "zero legacy references"? [Ambiguity, Spec Β§FR-007]
+
+## Cross-Artifact Consistency
+
+- [ ] CHK049 β Do task IDs in tasks.md (T001βT047) align with plan.md phase references and user story assignments? [Consistency, Cross-Artifact]
+- [ ] CHK050 β Do data-model.md tier classifications match the tier groupings in tasks.md Phases 3β5? [Consistency, Cross-Artifact]
+- [ ] CHK051 β Do quickstart.md migration patterns match the "After migration" column in data-model.md for all three tiers? [Consistency, Cross-Artifact]
+- [ ] CHK052 β Are success criteria (SC-001βSC-007) in spec.md traceable to specific verification tasks in tasks.md? [Traceability, Cross-Artifact]
+- [ ] CHK053 β Do the parallel execution opportunities documented in tasks.md (Β§Parallel Opportunities) align with the phase dependency constraints in plan.md? [Consistency, Cross-Artifact]
+
+## Constitution Compliance
+
+- [ ] CHK054 β Principle I (KMP Core): Are requirements specified to verify no `commonMain` source files are modified β only `build.gradle.kts` and convention plugin files? [Consistency, Spec Β§Source-Set Impact]
+- [ ] CHK055 β Principle II (Zero Lint Tolerance): Are `spotlessCheck` and `detekt` verification requirements included in the migration acceptance criteria? [Consistency, Plan Β§Constitution Check]
+- [ ] CHK056 β Principle IV (Privacy First): Is the `core/proto` submodule explicitly excluded from modifications in the requirements? [Consistency, Spec Β§Privacy Assessment]
+- [ ] CHK057 β Principle VI (Verify Before Push): Are all six verification commands from the constitution mapped to specific tasks? [Consistency, Plan Β§Constitution Check]
+
+---
+
+## Notes
+
+- Check items off as completed: `[x]`
+- Items marked `[Gap]` indicate requirements that may be missing entirely
+- Items marked `[Ambiguity]` or `[Conflict]` indicate requirements needing clarification before implementation
+- The 28-vs-27 module count discrepancy (CHK008, CHK045) is the most critical ambiguity β resolve in spec.md before proceeding
+- Cross-artifact consistency (CHK049βCHK053) is especially important given 5 design documents
diff --git a/specs/006-kmp-project-structure/checklists/requirements.md b/specs/006-kmp-project-structure/checklists/requirements.md
new file mode 100644
index 0000000000..ab8206d3ff
--- /dev/null
+++ b/specs/006-kmp-project-structure/checklists/requirements.md
@@ -0,0 +1,37 @@
+# Specification Quality Checklist: KMP Recommended Project Structure Alignment
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2025-07-15 (updated with blog post context)
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+- The Architecture section references specific plugin names, DSL block names, and file paths because this is a build infrastructure feature β these are the "domain concepts" of the feature, not implementation details. The spec remains technology-agnostic regarding *how* the migration is executed.
+- The "Design Standards Compliance" section was intentionally removed as this feature has zero UI impact.
+- Gap analysis is based on a concrete audit of all 30+ modules β findings are documented in the Architecture section.
+- All checklist items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
diff --git a/specs/006-kmp-project-structure/data-model.md b/specs/006-kmp-project-structure/data-model.md
new file mode 100644
index 0000000000..54393a4316
--- /dev/null
+++ b/specs/006-kmp-project-structure/data-model.md
@@ -0,0 +1,112 @@
+# Data Model: KMP Module Classification & Migration Matrix
+
+## Entity: KMP Module
+
+Each KMP module has the following properties relevant to migration:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `path` | String | Gradle module path (e.g., `:core:common`) |
+| `convention_plugin` | Enum | `meshtastic.kmp.library` or `meshtastic.kmp.feature` |
+| `namespace` | String? | Android namespace; null = auto-derived by convention |
+| `namespace_override_needed` | Boolean | True if auto-derived namespace doesn't match desired |
+| `android_resources` | Enum | `disabled` (default), `enabled`, `enabled_with_prefix` |
+| `host_test` | Enum | `none`, `empty`, `with_resources` |
+| `device_test` | Boolean | Has `withDeviceTest {}` block |
+| `min_sdk_override` | Int? | Non-null only if module overrides default minSdk |
+| `extra_jvm_target` | Boolean | Has explicit `jvm()` call (redundant with convention) |
+| `jvm_android_source_set` | Boolean | Uses `meshtastic.kmp.jvm.android` plugin |
+| `tier` | Enum | `simple`, `standard`, `special` |
+
+## Module Migration Matrix
+
+### Tier 1: Minimal β androidLibrary block becomes empty or removed (6 modules)
+
+Modules where convention handles everything. After migration, the `kotlin {}` block needs no `androidLibrary {}` at all (namespace auto-derived, resources disabled by convention, no tests).
+
+| Module | Current `android {}` content | After migration |
+|--------|------------------------------|-----------------|
+| `core:di` | namespace, resources=false | Remove `androidLibrary {}` entirely |
+| `core:nfc` | namespace, resources=false | Remove `androidLibrary {}` entirely |
+| `core:ui` | namespace, resources=false | Remove `androidLibrary {}` entirely |
+| `core:navigation` | namespace only | Remove `androidLibrary {}` entirely |
+| `feature:settings` | namespace, resources=false | Remove `androidLibrary {}` entirely |
+| `feature:messaging` | namespace, resources=false | Remove `androidLibrary {}` entirely |
+
+### Tier 2: Standard β only withHostTest remains (18 modules)
+
+Modules that opt into host tests. The `androidLibrary {}` block only contains `withHostTest {}`.
+
+| Module | withHostTest config | After migration |
+|--------|-------------------|-----------------|
+| `core:ble` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `core:common` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `core:data` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `core:domain` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `core:model` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `core:network` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `core:service` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `core:takserver` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `feature:connections` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `feature:firmware` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `feature:intro` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `feature:map` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `feature:node` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` |
+| `core:datastore` | `{}` (empty) | `androidLibrary { withHostTest {} }` |
+| `core:prefs` | `{}` (empty) | `androidLibrary { withHostTest {} }` |
+| `core:repository` | `{}` (empty) | `androidLibrary { withHostTest {} }` |
+| `core:testing` | `{}` (empty) | `androidLibrary { withHostTest {} }` |
+| `feature:wifi-provision` | `{}` (empty) | `androidLibrary { namespace = "org.meshtastic.feature.wifiprovision"; withHostTest {} }` |
+
+### Tier 3: Special β additional configuration (3 modules)
+
+| Module | Special config | After migration |
+|--------|---------------|-----------------|
+| `core:proto` | `minSdk = 21` | `androidLibrary { minSdk = 21 }` |
+| `core:database` | namespace + withHostTest + withDeviceTest | `androidLibrary { withHostTest { isIncludeAndroidResources = true }; withDeviceTest { instrumentationRunner = "..." } }` |
+| `core:resources` | resources enabled + prefix + withHostTest | `androidLibrary { androidResources { enable = true; resourcePrefix = "meshtastic_" }; withHostTest { isIncludeAndroidResources = true } }` |
+
+## Convention Plugin Enhancement
+
+### `configureKotlinMultiplatform()` β new default
+
+```
+BEFORE (KotlinAndroid.kt:91-101):
+ compileSdk = ...
+ minSdk = ...
+ namespace = auto-derived if null
+
+AFTER:
+ compileSdk = ...
+ minSdk = ...
+ namespace = auto-derived if null
+ androidResources.enable = false β NEW DEFAULT
+```
+
+This eliminates `androidResources.enable = false` from 23 of 27 module build files. Only `core:resources` overrides this to `true`.
+
+## Redundant `jvm()` cleanup
+
+14 modules declare `jvm()` in their `kotlin {}` block despite the convention plugin calling `jvm()` in `configureKotlinMultiplatform()`. These redundant declarations will be removed.
+
+Modules with redundant `jvm()`: `core:ble`, `core:common`, `core:data`, `core:database`, `core:datastore`, `core:di`, `core:domain`, `core:model`, `core:network`, `core:resources`, `core:takserver`, `core:ui`, `feature:map`, `feature:wifi-provision`.
+
+## State Transitions
+
+```
+Module State Machine:
+
+[LEGACY] [MIGRATED] [VERIFIED]
+android {} inside kotlin {} β androidLibrary {} DSL β Build passes
+ (or removed if empty)
+```
+
+Each module transitions independently. Build verification after each batch.
+
+## Validation Rules
+
+1. **No `android {}` inside `kotlin {}`**: After migration, `grep -rn "kotlin {" -A5 | grep "android {"` must return zero matches in KMP modules.
+2. **No redundant `jvm()`**: After cleanup, only convention plugin declares `jvm()`.
+3. **Namespace correctness**: All modules must resolve to correct namespace (verified by successful build).
+4. **Resources disabled by default**: Only `core:resources` has `androidResources.enable = true`.
+5. **Build verification**: `assembleDebug`, `:desktop:packageUberJarForCurrentOS`, `allTests`, and `DESKTOP_ONLY` mode all pass.
diff --git a/specs/006-kmp-project-structure/plan.md b/specs/006-kmp-project-structure/plan.md
new file mode 100644
index 0000000000..80cbb82288
--- /dev/null
+++ b/specs/006-kmp-project-structure/plan.md
@@ -0,0 +1,78 @@
+# Implementation Plan: KMP Recommended Project Structure Alignment
+
+**Branch**: `020-kmp-project-structure` | **Date**: 2025-07-15 | **Spec**: `specs/006-kmp-project-structure/spec.md`
+**Input**: Feature specification from `specs/006-kmp-project-structure/spec.md`
+
+## Summary
+
+Migrate 27 KMP modules from legacy `android {}` blocks inside `kotlin {}` to the recommended `kotlin.androidLibrary {}` top-level DSL. The project already runs AGP 9.2.1 and applies `com.android.kotlin.multiplatform.library` via convention plugins β the primary work is DSL migration in individual module `build.gradle.kts` files and hardening the convention plugin helper `configureKotlinMultiplatform()` to absorb common settings (like `androidResources.enable = false` and `withHostTest {}`) so modules need fewer per-module overrides. Build verification at each step is critical.
+
+## Technical Context
+
+**Language/Version**: Kotlin 2.3+ targeting JDK 21 (JDK 17 for published modules)
+**Primary Dependencies**: AGP 9.2.1, Kotlin Multiplatform Gradle Plugin, `com.android.kotlin.multiplatform.library`
+**Storage**: N/A (build infrastructure only)
+**Testing**: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` for build verification
+**Target Platform**: Android (API 26+) + Compose Desktop (JVM)
+**Project Type**: Mobile app (KMP) β build infrastructure change
+**Performance Goals**: Build time must not increase >5% (NFR-001)
+**Constraints**: Configuration cache, isolated projects, parallel execution must remain functional (NFR-002)
+**Scale/Scope**: 27 KMP modules across `core/` (19) and `feature/` (8), plus 4 convention plugin files in `build-logic/`
+
+## Constitution Check
+
+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
+
+- **I. Kotlin Multiplatform Core**: β
PASS β No source files are modified. All changes are to `build.gradle.kts` configuration files and convention plugin Kotlin files in `build-logic/`. Business logic in `commonMain` is untouched. The migration moves Android target configuration from `kotlin { android {} }` to `kotlin { androidLibrary {} }` β a pure DSL change with identical semantics.
+
+- **II. Zero Lint Tolerance**: β
PASS β Verification commands:
+ - `./gradlew spotlessApply spotlessCheck` (formatting)
+ - `./gradlew detekt` (static analysis)
+ - Convention plugin files in `build-logic/` are covered by the `:build-logic:convention:spotlessCheck` and `:build-logic:convention:detekt` tasks.
+
+- **III. Compose Multiplatform UI**: β
N/A β No UI changes. This is a build infrastructure change only.
+
+- **IV. Privacy First**: β
PASS β No runtime behavior changes. No PII exposure. `core/proto` submodule not modified.
+
+- **V. Design Standards Compliance**: β
N/A β No user-facing UI changes. Cross-Platform Spec: N/A β build infrastructure change.
+
+- **VI. Verify Before Push**: β
β Local verification commands:
+ ```bash
+ ./gradlew spotlessApply spotlessCheck detekt assembleDebug :desktop:packageUberJarForCurrentOS allTests
+ DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS
+ ```
+ Post-push: `gh pr checks <PR>` or `gh run list --branch 020-kmp-project-structure --limit 5`
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/006-kmp-project-structure/
+βββ plan.md # This file
+βββ research.md # Phase 0: DSL migration research
+βββ data-model.md # Phase 1: Module classification and migration matrix
+βββ quickstart.md # Phase 1: Step-by-step migration guide
+βββ tasks.md # Phase 2 output (/speckit.tasks command)
+```
+
+### Source Code (repository root)
+
+```text
+build-logic/convention/src/main/kotlin/
+βββ KmpLibraryConventionPlugin.kt # Convention plugin β no changes expected
+βββ KmpFeatureConventionPlugin.kt # Convention plugin β no changes expected
+βββ KmpJvmAndroidConventionPlugin.kt # Convention plugin β no changes expected
+βββ org/meshtastic/buildlogic/
+ βββ KotlinAndroid.kt # configureKotlinMultiplatform() β harden defaults
+
+core/*/build.gradle.kts # 19 KMP modules β migrate android {} β androidLibrary {}
+feature/*/build.gradle.kts # 8 KMP modules β migrate android {} β androidLibrary {}
+ (excluding feature/widget/ β Android-only, not affected)
+```
+
+**Structure Decision**: Existing module structure is preserved. This change only modifies `build.gradle.kts` files and convention plugin helpers. No directory restructuring.
+
+## Complexity Tracking
+
+> No constitution violations. No complexity exceptions needed.
diff --git a/specs/006-kmp-project-structure/quickstart.md b/specs/006-kmp-project-structure/quickstart.md
new file mode 100644
index 0000000000..fcd535a1ea
--- /dev/null
+++ b/specs/006-kmp-project-structure/quickstart.md
@@ -0,0 +1,189 @@
+# Quickstart: KMP Recommended Project Structure Migration
+
+## Prerequisites
+
+- JDK 21 installed
+- `ANDROID_HOME` set
+- Proto submodule initialized: `git submodule update --init`
+- `local.properties` exists: `[ -f local.properties ] || cp secrets.defaults.properties local.properties`
+- Clean build baseline: `./gradlew assembleDebug` passes before starting
+
+## Migration Steps
+
+### Step 1: Harden convention plugin defaults
+
+**File**: `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt`
+
+In `configureKotlinMultiplatform()`, add `androidResources.enable = false` as default inside the `pluginManager.withPlugin` block:
+
+```kotlin
+pluginManager.withPlugin("com.android.kotlin.multiplatform.library") {
+ extensions.findByType<KotlinMultiplatformAndroidLibraryTarget>()?.apply {
+ compileSdk = configProperties.getProperty("COMPILE_SDK").toInt()
+ minSdk = configProperties.getProperty("MIN_SDK").toInt()
+ androidResources.enable = false // β NEW: Default for all KMP modules
+ if (namespace == null) {
+ val pkg = this@configureKotlinMultiplatform.path.removePrefix(":").replace(":", ".")
+ namespace = "org.meshtastic.$pkg"
+ }
+ }
+}
+```
+
+**Verify**: `./gradlew assembleDebug` β should pass (no behavior change yet, modules still override).
+
+### Step 2: Migrate Tier 1 modules (minimal β 6 modules)
+
+For each module, change `android {}` to `androidLibrary {}` inside `kotlin {}`, removing properties now handled by convention.
+
+**Pattern β before**:
+```kotlin
+kotlin {
+ jvm() // β remove if present (convention handles it)
+
+ android {
+ namespace = "org.meshtastic.core.di" // β auto-derived, remove
+ androidResources.enable = false // β convention default, remove
+ }
+ // ...
+}
+```
+
+**Pattern β after**:
+```kotlin
+kotlin {
+ // android target configured by convention plugin (namespace auto-derived, resources disabled)
+ sourceSets { /* ... */ }
+}
+```
+
+**Modules**: `core:di`, `core:nfc`, `core:ui`, `core:navigation`, `feature:messaging`, `feature:settings`
+
+**Verify after each module**: `./gradlew :<module>:assembleDebug :<module>:allTests`
+**Verify after batch**: `./gradlew assembleDebug allTests`
+
+### Step 3: Migrate Tier 2 modules (withHostTest β 18 modules)
+
+**Pattern β before**:
+```kotlin
+kotlin {
+ jvm()
+
+ android {
+ namespace = "org.meshtastic.core.data"
+ androidResources.enable = false
+ withHostTest { isIncludeAndroidResources = true }
+ }
+ // ...
+}
+```
+
+**Pattern β after**:
+```kotlin
+kotlin {
+ androidLibrary {
+ withHostTest { isIncludeAndroidResources = true }
+ }
+ // ...
+}
+```
+
+**Special case β `feature:wifi-provision`** (namespace mismatch):
+```kotlin
+kotlin {
+ androidLibrary {
+ namespace = "org.meshtastic.feature.wifiprovision"
+ withHostTest {}
+ }
+ // ...
+}
+```
+
+**Verify after batch**: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests`
+
+### Step 4: Migrate Tier 3 modules (special β 3 modules)
+
+#### `core:proto`:
+```kotlin
+kotlin {
+ androidLibrary {
+ minSdk = 21 // ATAK compatibility override
+ }
+ // ...
+}
+```
+
+#### `core:database`:
+```kotlin
+kotlin {
+ androidLibrary {
+ withHostTest { isIncludeAndroidResources = true }
+ withDeviceTest { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }
+ }
+ // ...
+}
+```
+
+#### `core:resources`:
+```kotlin
+kotlin {
+ androidLibrary {
+ androidResources {
+ enable = true // Override convention default
+ resourcePrefix = "meshtastic_"
+ }
+ withHostTest { isIncludeAndroidResources = true }
+ }
+ // ...
+}
+```
+
+**Verify**: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests`
+
+### Step 5: Full verification
+
+```bash
+# Full Android build
+./gradlew assembleDebug
+
+# Full Desktop build
+./gradlew :desktop:packageUberJarForCurrentOS
+
+# All tests
+./gradlew allTests
+
+# Desktop-only mode (no Android SDK)
+DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS
+
+# Lint and formatting
+./gradlew spotlessApply spotlessCheck detekt
+
+# Verify no legacy android {} blocks remain in KMP modules
+grep -rn "android {" core/*/build.gradle.kts feature/*/build.gradle.kts | grep -v "widget\|api\|barcode\|androidLibrary\|androidResources\|androidMain\|androidHostTest\|androidDeviceTest\|androidRuntimeClasspath"
+# Expected: zero matches
+```
+
+### Step 6: Validate success criteria
+
+- [ ] SC-001: Zero `android {}` blocks inside `kotlin {}` in KMP modules
+- [ ] SC-002: `assembleDebug` and `:desktop:packageUberJarForCurrentOS` pass
+- [ ] SC-003: `allTests` passes with zero regressions
+- [ ] SC-004: `DESKTOP_ONLY` build succeeds
+- [ ] SC-005: Clean `assembleDebug` time within 5% of baseline
+- [ ] SC-006: New module can apply `meshtastic.kmp.library` with no manual `android {}` block
+- [ ] SC-007: `configureKotlinMultiplatform()` uses only `KotlinMultiplatformAndroidLibraryTarget` API
+
+## Rollback Plan
+
+If any step produces build failures that cannot be resolved:
+
+**Per-module**: `git checkout -- <module>/build.gradle.kts` to revert a single module migration.
+
+**Per-tier**:
+- Tier 1: `git checkout -- core/di/build.gradle.kts core/nfc/build.gradle.kts core/ui/build.gradle.kts core/navigation/build.gradle.kts feature/settings/build.gradle.kts feature/messaging/build.gradle.kts`
+- Tier 2: `git checkout -- core/{ble,common,data,domain,model,network,service,takserver,datastore,prefs,repository,testing}/build.gradle.kts feature/{connections,firmware,intro,map,node,wifi-provision}/build.gradle.kts`
+- Tier 3: `git checkout -- core/{proto,database,resources}/build.gradle.kts`
+
+**Full rollback**: `git stash` or `git checkout -- .` to revert all changes.
+
+Convention plugin change (Step 1) is backward-compatible β existing explicit `androidResources.enable = false` in modules is a no-op override.
diff --git a/specs/006-kmp-project-structure/research.md b/specs/006-kmp-project-structure/research.md
new file mode 100644
index 0000000000..3d29b58ea4
--- /dev/null
+++ b/specs/006-kmp-project-structure/research.md
@@ -0,0 +1,166 @@
+# Research: KMP Recommended Project Structure Alignment
+
+## R1: `android {}` vs `androidLibrary {}` inside `kotlin {}`
+
+**Decision**: Use `androidLibrary {}` as the canonical DSL accessor for the KMP Android target.
+
+**Rationale**: The `androidLibrary {}` accessor is the documented API in the [KMP recommended structure guide](https://kotlinlang.org/docs/multiplatform/multiplatform-project-recommended-structure.html) and the [KMP-App-Template](https://github.com/Kotlin/KMP-App-Template). While `android {}` inside `kotlin {}` is a transitional alias that still works (used by kotlinconf-app), `androidLibrary {}` is the forward-looking name that clearly distinguishes between:
+- `kotlin { androidLibrary {} }` β KMP module Android target (new plugin)
+- `android {}` β top-level Android extension (legacy `com.android.library` or `com.android.application`)
+
+Using `androidLibrary {}` eliminates ambiguity and signals the project uses the recommended patterns.
+
+**Alternatives considered**:
+- Keep `android {}` inside `kotlin {}` (works but ambiguous, transitional)
+- Use programmatic API only in convention plugins (already done for compileSdk/minSdk; per-module overrides still need DSL)
+
+## R2: What properties belong in `kotlin.androidLibrary {}` vs convention plugin
+
+**Decision**: Convention plugin (`configureKotlinMultiplatform()`) handles:
+- `compileSdk` β
(already done)
+- `minSdk` β
(already done, default from config.properties)
+- `namespace` auto-derivation β
(already done when null)
+- `androidResources.enable = false` as default β NEW (absorb into convention)
+
+Module `build.gradle.kts` handles only overrides:
+- `namespace` when it differs from auto-derived (e.g., `feature.wifiprovision` vs `feature.wifi-provision`)
+- `minSdk` override (only `core:proto` with minSdk 21)
+- `androidResources { enable = true; resourcePrefix = "..." }` (only `core:resources`)
+- `withHostTest {}` / `withDeviceTest {}` (per-module opt-in)
+
+**Rationale**: The convention plugin already handles compileSdk/minSdk/namespace via `KotlinMultiplatformAndroidLibraryTarget`. Adding `androidResources.enable = false` as the convention default eliminates the most common per-module boilerplate (23 of 27 modules set this).
+
+**Alternatives considered**:
+- Move all config to convention plugin, including withHostTest (rejected: test opt-in should be explicit per-module)
+- Keep all config in module files (rejected: adds repetitive boilerplate across 27 modules)
+
+## R3: Convention plugin audit β what needs changing
+
+**Decision**: The `configureKotlinMultiplatform()` function in `KotlinAndroid.kt` is already well-structured. One enhancement needed:
+
+1. **Add `androidResources.enable = false` as default** in the `pluginManager.withPlugin` block alongside compileSdk/minSdk/namespace. This eliminates the most common boilerplate line across 23 modules.
+
+The `KmpLibraryConventionPlugin.kt`, `KmpFeatureConventionPlugin.kt`, and `KmpJvmAndroidConventionPlugin.kt` do NOT need changes β they compose correctly and delegate to `configureKotlinMultiplatform()`.
+
+**Rationale**: The convention plugin chain is already correct:
+- `KmpLibraryConventionPlugin` applies `com.android.kotlin.multiplatform.library` (correct plugin)
+- `configureKotlinMultiplatform()` uses `KotlinMultiplatformAndroidLibraryTarget` API (correct API)
+- `isDesktopOnly` guard is properly implemented
+- The only gap is that individual modules repeat `androidResources.enable = false`
+
+**Alternatives considered**:
+- Rewrite convention plugins to use DSL instead of programmatic API (rejected: programmatic API via `KotlinMultiplatformAndroidLibraryTarget` is cleaner in convention plugins and already works)
+- Add `withHostTest {}` to convention by default (rejected: not all modules need host tests, and some configure `isIncludeAndroidResources = true`)
+
+## R4: Module-by-module android {} block classification
+
+**Decision**: Categorize all 27 modules into migration tiers based on complexity.
+
+### Tier 1: Simple β namespace + androidResources.enable = false (6 modules)
+These modules only set namespace and disable resources. After convention absorbs `androidResources.enable = false`, these only need namespace (or nothing if auto-derived matches).
+
+| Module | namespace | Auto-derivable? |
+|--------|-----------|-----------------|
+| `core:di` | `org.meshtastic.core.di` | β
Yes |
+| `core:nfc` | `org.meshtastic.core.nfc` | β
Yes |
+| `core:ui` | `org.meshtastic.core.ui` | β
Yes |
+| `core:navigation` | `org.meshtastic.core.navigation` | β
Yes |
+| `feature:messaging` | `org.meshtastic.feature.messaging` | β
Yes |
+| `feature:settings` | `org.meshtastic.feature.settings` | β
Yes |
+
+### Tier 2: Namespace + resources disabled + withHostTest (18 modules)
+These add `withHostTest {}` opt-in. The `androidLibrary {}` block will keep withHostTest.
+
+| Module | withHostTest config |
+|--------|-------------------|
+| `core:ble` | `{ isIncludeAndroidResources = true }` |
+| `core:common` | `{ isIncludeAndroidResources = true }` |
+| `core:data` | `{ isIncludeAndroidResources = true }` |
+| `core:domain` | `{ isIncludeAndroidResources = true }` |
+| `core:model` | `{ isIncludeAndroidResources = true }` |
+| `core:network` | `{ isIncludeAndroidResources = true }` |
+| `core:service` | `{ isIncludeAndroidResources = true }` |
+| `core:takserver` | `{ isIncludeAndroidResources = true }` |
+| `feature:connections` | `{ isIncludeAndroidResources = true }` |
+| `feature:firmware` | `{ isIncludeAndroidResources = true }` |
+| `feature:intro` | `{ isIncludeAndroidResources = true }` |
+| `feature:map` | `{ isIncludeAndroidResources = true }` |
+| `feature:node` | `{ isIncludeAndroidResources = true }` |
+| `core:datastore` | `{}` (empty) |
+| `core:prefs` | `{}` (empty) |
+| `core:repository` | `{}` (empty) |
+| `core:testing` | `{}` (empty) |
+| `feature:wifi-provision` | `{}` (empty) |
+
+### Tier 3: Special cases (3 modules)
+| Module | Special config |
+|--------|---------------|
+| `core:proto` | `minSdk = 21` override (for ATAK compatibility) |
+| `core:database` | `namespace` + `withHostTest` + `withDeviceTest { instrumentationRunner }` |
+| `core:resources` | `androidResources { enable = true; resourcePrefix = "meshtastic_" }` + `withHostTest` |
+
+### Not affected (correctly Android-only)
+- `core:api` β `meshtastic.android.library`, AIDL, publishing
+- `core:barcode` β `meshtastic.android.library`, Android-only
+- `feature:widget` β `meshtastic.android.library`, Glance widgets
+- `app/` β `com.android.application`
+- `desktop/` β JVM only
+- `screenshot-tests/` β Android test module
+
+## R5: DESKTOP_ONLY mode compatibility
+
+**Decision**: No changes needed to DESKTOP_ONLY mode. The migration is purely a DSL rename.
+
+**Rationale**:
+- `isDesktopOnly` guard in `KmpLibraryConventionPlugin` skips `com.android.kotlin.multiplatform.library` plugin application entirely
+- `configureKotlinMultiplatform()` creates inert placeholder `androidMain` source set in desktop-only mode
+- The `kotlin { androidLibrary {} }` DSL accessor in module build files is provided by the plugin β when the plugin isn't applied (desktop-only), the accessor doesn't exist
+- The existing `android {}` accessor inside `kotlin {}` works the same way β so the rename from `android {}` to `androidLibrary {}` has identical desktop-only behavior
+
+**Key validation**: After migration, `DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS` must succeed.
+
+## R6: `jvm()` declaration in module build files
+
+**Decision**: Remove redundant `jvm()` from module build files as a cleanup during migration β the convention plugin handles it. Calling `jvm()` twice is a no-op (Kotlin Gradle Plugin is idempotent for target declarations), so removing it is safe. This reduces boilerplate and makes convention ownership clear.
+
+**Alternatives considered**: Keep `jvm()` in all modules for explicitness (rejected: inconsistent since some modules already omit it, and convention plugins are the source of truth).
+
+## R7: Namespace auto-derivation coverage
+
+**Decision**: Rely on convention plugin auto-derivation for most modules; only override where the derived name doesn't match.
+
+| Module path | Auto-derived namespace | Needed namespace | Match? |
+|-------------|----------------------|------------------|--------|
+| `:core:ble` | `org.meshtastic.core.ble` | `org.meshtastic.core.ble` | β
|
+| `:core:common` | `org.meshtastic.core.common` | `org.meshtastic.core.common` | β
|
+| `:core:data` | `org.meshtastic.core.data` | `org.meshtastic.core.data` | β
|
+| `:core:database` | `org.meshtastic.core.database` | `org.meshtastic.core.database` | β
|
+| `:core:datastore` | `org.meshtastic.core.datastore` | `org.meshtastic.core.datastore` | β
|
+| `:core:di` | `org.meshtastic.core.di` | `org.meshtastic.core.di` | β
|
+| `:core:domain` | `org.meshtastic.core.domain` | `org.meshtastic.core.domain` | β
|
+| `:core:model` | `org.meshtastic.core.model` | (none set β auto-derived) | β
|
+| `:core:navigation` | `org.meshtastic.core.navigation` | `org.meshtastic.core.navigation` | β
|
+| `:core:network` | `org.meshtastic.core.network` | `org.meshtastic.core.network` | β
|
+| `:core:nfc` | `org.meshtastic.core.nfc` | `org.meshtastic.core.nfc` | β
|
+| `:core:prefs` | `org.meshtastic.core.prefs` | `org.meshtastic.core.prefs` | β
|
+| `:core:proto` | `org.meshtastic.core.proto` | (none set β auto-derived) | β
|
+| `:core:repository` | `org.meshtastic.core.repository` | (none set β auto-derived) | β
|
+| `:core:resources` | `org.meshtastic.core.resources` | (none set β auto-derived) | β
|
+| `:core:service` | `org.meshtastic.core.service` | `org.meshtastic.core.service` | β
|
+| `:core:takserver` | `org.meshtastic.core.takserver` | `org.meshtastic.core.takserver` | β
|
+| `:core:testing` | `org.meshtastic.core.testing` | `org.meshtastic.core.testing` | β
|
+| `:core:ui` | `org.meshtastic.core.ui` | `org.meshtastic.core.ui` | β
|
+| `:feature:connections` | `org.meshtastic.feature.connections` | `org.meshtastic.feature.connections` | β
|
+| `:feature:firmware` | `org.meshtastic.feature.firmware` | `org.meshtastic.feature.firmware` | β
|
+| `:feature:intro` | `org.meshtastic.feature.intro` | `org.meshtastic.feature.intro` | β
|
+| `:feature:map` | `org.meshtastic.feature.map` | `org.meshtastic.feature.map` | β
|
+| `:feature:messaging` | `org.meshtastic.feature.messaging` | `org.meshtastic.feature.messaging` | β
|
+| `:feature:node` | `org.meshtastic.feature.node` | `org.meshtastic.feature.node` | β
|
+| `:feature:settings` | `org.meshtastic.feature.settings` | `org.meshtastic.feature.settings` | β
|
+| `:feature:wifi-provision` | `org.meshtastic.feature.wifi.provision` | `org.meshtastic.feature.wifiprovision` | β |
+
+**Finding**: Only `feature:wifi-provision` has a namespace mismatch β the module path contains a hyphen which auto-derives as `org.meshtastic.feature.wifi.provision` (with dot separator) but the module currently uses `org.meshtastic.feature.wifiprovision` (no dot). This module MUST explicitly set `namespace` in its `androidLibrary {}` block.
+
+All other modules that currently set `namespace` explicitly match the auto-derived value and can safely rely on the convention plugin's auto-derivation. However, being explicit about namespace provides documentation value and prevents silent changes if the module is moved.
+
+**Decision**: Remove explicit `namespace` from modules where it matches auto-derivation. Keep it only for `feature:wifi-provision`. This reduces boilerplate and delegates ownership to the convention plugin.
diff --git a/specs/006-kmp-project-structure/spec.md b/specs/006-kmp-project-structure/spec.md
new file mode 100644
index 0000000000..5d30364e9e
--- /dev/null
+++ b/specs/006-kmp-project-structure/spec.md
@@ -0,0 +1,258 @@
+# Feature Specification: KMP Recommended Project Structure Alignment
+
+**Feature Branch**: `020-kmp-project-structure`
+**Created**: 2025-07-15
+**Status**: Draft
+**Input**: User description: "Restructure Meshtastic Android project to align with the updated Kotlin Multiplatform recommended project structure"
+**Cross-Platform Spec**: N/A β this is a build/infrastructure change with no user-facing behavior changes across platforms
+
+## Summary
+
+The Meshtastic Android project should be aligned with the official Kotlin Multiplatform recommended project structure as documented in the [KMP project structure guide](https://kotlinlang.org/docs/multiplatform/multiplatform-project-recommended-structure.html) and the [JetBrains blog post on the new KMP default structure](https://blog.jetbrains.com/kotlin/2026/05/new-kmp-default-structure/). The project already runs on AGP 9.2.1 and already applies the `com.android.kotlin.multiplatform.library` plugin via convention plugins, but an audit reveals that **all 27 KMP modules still contain legacy `android {}` blocks inside `kotlin {}`** that should be migrated to the recommended `kotlin.androidLibrary {}` top-level DSL. This effort focuses on completing that migration, validating module boundaries, and ensuring convention plugins enforce the canonical patterns β so the project fully conforms to the new structure with clear module responsibilities.
+
+## Goals
+
+1. **Migrate all legacy `android {}` blocks** inside `kotlin {}` to the recommended `kotlin.androidLibrary {}` top-level DSL across all 27 KMP modules in `core/` and `feature/`.
+2. **Validate and harden convention plugins** so that `KmpLibraryConventionPlugin` and `KmpFeatureConventionPlugin` configure Android targets exclusively through the new plugin's DSL, preventing legacy patterns from being reintroduced.
+3. **Confirm entry-point module separation** β `app/` (Android) and `desktop/` (JVM) are already separate entry-point modules with no shared business logic, satisfying the AGP 9 mandatory requirement.
+4. **Document the module boundary model** β map the current `core/` (shared logic) and `feature/` (shared UI + logic) organization to the recommended `sharedLogic` / `sharedUI` categories, confirming they already satisfy the recommended split.
+5. **Ensure forward compatibility** β verify the project structure is compatible with future KMP toolchain updates and potential new targets (iOS, web) without requiring another structural overhaul.
+
+## Non-Goals
+
+- **Introducing iOS, web, or server modules** β while the recommended structure supports these, this spec scopes only the existing Android + Desktop targets.
+- **Renaming modules to match the default template names** β the recommended structure uses `shared`, `androidApp`, `desktopApp` as defaults, but the project's existing `core/`, `feature/`, `app/`, `desktop/` naming is equally valid and will not be renamed for cosmetic alignment.
+- **Changing application behavior or UI** β this is purely a build infrastructure and module organization change. No user-facing functionality changes.
+- **Migrating away from existing technology choices** β Koin, Ktor, Room KMP, Compose Multiplatform, and Navigation 3 remain as-is per the constitution.
+- **Migrating `feature:widget` away from `com.android.library`** β this module is genuinely Android-only (Glance app widgets) and correctly uses the Android library plugin, not the KMP library plugin.
+
+## User Scenarios & Testing *(mandatory)*
+
+### User Story 1 - Developer Builds Successfully After Restructuring (Priority: P1)
+
+As a contributor, I want the project to build successfully on both Android and Desktop targets after any structural changes, so that my development workflow is uninterrupted.
+
+**Why this priority**: A broken build blocks all development. This is the absolute minimum bar for any structural change.
+
+**Independent Test**: Can be fully tested by running `./gradlew assembleDebug`, `./gradlew :desktop:packageUberJarForCurrentOS`, and `./gradlew allTests` and verifying all pass.
+
+**Acceptance Scenarios**:
+
+1. **Given** the restructured project, **When** a developer runs the full Android build (`assembleDebug`), **Then** the build completes with zero errors.
+2. **Given** the restructured project, **When** a developer runs the Desktop build (`:desktop:packageUberJarForCurrentOS`), **Then** the build completes with zero errors.
+3. **Given** the restructured project, **When** a developer runs all tests (`allTests`), **Then** all existing tests pass with no regressions.
+4. **Given** the restructured project with `DESKTOP_ONLY=true`, **When** a developer builds without Android SDK, **Then** the desktop-only build succeeds as before.
+
+---
+
+### User Story 2 - Convention Plugins Reflect Recommended Patterns (Priority: P2)
+
+As a build maintainer, I want the convention plugins in `build-logic/` to apply the correct KMP plugin configuration patterns recommended by JetBrains, so that adding new modules follows a clear, documented convention.
+
+**Why this priority**: Convention plugins are the enforcement layer for project structure. If they are correct, individual module configurations stay consistent.
+
+**Independent Test**: Can be tested by verifying that `KmpLibraryConventionPlugin` applies the `com.android.kotlin.multiplatform.library` plugin and that no KMP library module uses the legacy `com.android.library` plugin.
+
+**Acceptance Scenarios**:
+
+1. **Given** the `meshtastic.kmp.library` convention plugin, **When** applied to a KMP module, **Then** it configures the module using `com.android.kotlin.multiplatform.library` (not `com.android.library`).
+2. **Given** the `meshtastic.kmp.feature` convention plugin, **When** applied to a feature module, **Then** it inherits the correct KMP library plugin chain without legacy Android library configuration.
+3. **Given** a new core module is created, **When** a developer applies `meshtastic.kmp.library`, **Then** the module follows the recommended structure with `kotlin.androidLibrary {}` configuration.
+
+---
+
+### User Story 3 - Module Boundary Clarity for New Contributors (Priority: P3)
+
+As a new contributor, I want a clear separation between entry-point modules, shared business logic modules, and shared UI modules, so that I know where to add new code based on its purpose.
+
+**Why this priority**: Clear boundaries reduce onboarding friction and prevent architectural drift over time.
+
+**Independent Test**: Can be tested by reviewing the module dependency graph and verifying that entry-point modules (`app/`, `desktop/`) do not contain shared logic, and that `core/` modules do not depend on `feature/` modules.
+
+**Acceptance Scenarios**:
+
+1. **Given** the module dependency graph, **When** analyzing `app/` dependencies, **Then** `app/` depends on `core/` and `feature/` modules but contains no shared business logic.
+2. **Given** the module dependency graph, **When** analyzing `desktop/` dependencies, **Then** `desktop/` depends on `core/` and `feature/` modules but contains no shared business logic.
+3. **Given** any `core/` module, **When** examining its dependencies, **Then** it does not depend on any `feature/` module (unidirectional flow preserved).
+
+---
+
+### User Story 4 - Legacy DSL Block Migration (Priority: P1)
+
+As a build maintainer, I want all 27 KMP modules' legacy `android {}` blocks inside `kotlin {}` migrated to the recommended `kotlin.androidLibrary {}` top-level DSL, so that the build configuration fully uses the new plugin's canonical API.
+
+**Why this priority**: The project already runs AGP 9.2.1 and applies the `com.android.kotlin.multiplatform.library` plugin, but every KMP module still configures its Android target using the legacy `android {}` block inside `kotlin {}` (for namespace, resource settings, etc.). This is the primary gap between the current state and full alignment with the recommended structure. It ties with P1 because the build already works β this is about eliminating technical debt before it becomes a blocker.
+
+**Independent Test**: Can be tested by searching all `core/` and `feature/` KMP module `build.gradle.kts` files for `android {` blocks inside `kotlin {}` β zero should remain after migration. All configuration should appear in `kotlin.androidLibrary {}` top-level blocks or be handled by convention plugins.
+
+**Acceptance Scenarios**:
+
+1. **Given** any KMP library module in `core/` or `feature/`, **When** examining its `build.gradle.kts`, **Then** it contains no `android {}` block inside `kotlin {}` β Android configuration uses the `kotlin.androidLibrary {}` top-level DSL or is delegated to convention plugins.
+2. **Given** the `configureKotlinMultiplatform()` helper in build-logic, **When** it configures Android targets, **Then** it uses `KotlinMultiplatformAndroidLibraryTarget` APIs from the new plugin, not legacy `android {}` extension configuration.
+3. **Given** the full project with all 27 modules migrated, **When** building Android and Desktop targets, **Then** all builds succeed with zero errors.
+
+---
+
+### Edge Cases
+
+- What happens when the `DESKTOP_ONLY` mode is active and no Android SDK is available? The restructured plugins must continue to conditionally skip Android plugin application (as the current `isDesktopOnly` guard already does).
+- How does the system handle modules that are genuinely Android-only (e.g., `core:api`, `core:barcode`, `screenshot-tests`)? These modules should continue using `com.android.library` or `com.android.application` as appropriate β the KMP library plugin applies only to multiplatform modules.
+- What happens if a module currently uses `com.android.library` but should be KMP? The migration path must be documented and executed per-module, with build verification at each step.
+
+## Architecture
+
+### Gap Analysis: Current State vs Recommended Structure
+
+The audit reveals the project is **substantially aligned** with the recommended KMP structure, with one significant gap remaining:
+
+```
+AREA STATUS FINDING
+==== ====== =======
+
+Entry-point separation β
DONE app/ (Android) and desktop/ (JVM) are
+ already separate entry-point modules.
+ AGP 9 mandatory requirement satisfied.
+
+KMP library plugin adoption β
DONE KmpLibraryConventionPlugin already applies
+ com.android.kotlin.multiplatform.library
+ via the android-kotlin-multiplatform-library
+ catalog alias. No androidTarget {} calls.
+
+AGP version β
DONE Project already runs AGP 9.2.1.
+
+Legacy android {} blocks in kotlin {} β οΈ GAP All 27 KMP modules still configure Android
+ via android {} blocks INSIDE kotlin {} for
+ namespace, androidResources, withHostTest.
+ These should migrate to kotlin.androidLibrary {}
+ top-level DSL blocks.
+
+Module boundary model β
DONE core/ = shared business logic (β sharedLogic)
+ feature/ = shared UI + logic (β sharedUI)
+ Unidirectional: app/desktop β feature β core
+
+Dependency direction β
DONE No reverse dependencies. core/ does not
+ depend on feature/. Unidirectional flow.
+
+DESKTOP_ONLY mode β
DONE Conditional Android plugin skipping via
+ isDesktopOnly guard in convention plugins.
+
+Android-only modules β
DONE feature:widget uses com.android.library
+ correctly. core:api, core:barcode also
+ Android-only with appropriate plugins.
+
+Convention plugin architecture β οΈ GAP configureKotlinMultiplatform() helper
+ may contain legacy android {} configuration
+ patterns alongside the new plugin's API.
+ Needs audit and cleanup.
+```
+
+### Affected Modules (Legacy `android {}` blocks to migrate)
+
+**Core modules (19)**:
+`core:ble`, `core:common`, `core:data`, `core:database`, `core:datastore`, `core:di`, `core:domain`, `core:model`, `core:navigation`, `core:network`, `core:nfc`, `core:prefs`, `core:proto`, `core:repository`, `core:resources`, `core:service`, `core:takserver`, `core:testing`, `core:ui`
+
+**Feature modules (8 β excluding widget)**:
+`feature:connections`, `feature:firmware`, `feature:intro`, `feature:map`, `feature:messaging`, `feature:node`, `feature:settings`, `feature:wifi-provision`
+
+**Not affected** (correctly using `com.android.library` or `com.android.application`):
+`app/`, `desktop/`, `feature:widget`, `core:api`, `core:barcode`, `screenshot-tests`
+
+### Mapping to Recommended Structure
+
+```
+RECOMMENDED STRUCTURE MESHTASTIC EQUIVALENT NOTES
+===================== ======================= =====
+
+androidApp/ app/ β
Same role, different name
+ - kotlin.android (naming is cosmetic)
+ + com.android.application
+
+desktopApp/ desktop/ β
Same role, different name
+ - kotlin.jvm + compose
+
+shared/ (single shared mod) core/ + feature/ β
Already modularized further
+ OR (27+ KMP modules) than the default; this is the
+sharedLogic/ + sharedUI/ core/ β sharedLogic recommended "advanced" pattern
+ feature/ β sharedUI
+
+build-logic/ build-logic/ β
Project-specific convention
+ plugins (not prescribed by
+ JetBrains but aligned)
+```
+
+### Reference Projects Comparison
+
+The JetBrains blog cites `kotlinconf-app`, `KMP-App-Template`, and `RSS Reader` as reference implementations. The Meshtastic project is more mature and more modularized than any of these, having already decomposed the monolithic `shared` module into granular `core/` and `feature/` modules. This is the recommended evolution path for larger projects.
+
+### Key Components
+
+| Component | Module / File | Purpose |
+| -------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------- |
+| KmpLibraryConventionPlugin | `build-logic/convention/.../KmpLibraryConventionPlugin.kt` | Applies KMP + Android library plugins to shared modules |
+| KmpFeatureConventionPlugin | `build-logic/convention/.../KmpFeatureConventionPlugin.kt` | Composite plugin for feature modules (KMP + Compose + Koin) |
+| KmpJvmAndroidConventionPlugin | `build-logic/convention/.../KmpJvmAndroidConventionPlugin.kt` | Configures jvmAndroidMain shared source set |
+| AndroidLibraryConventionPlugin | `build-logic/convention/.../AndroidLibraryConventionPlugin.kt` | Legacy Android library plugin (for Android-only modules) |
+| configureKotlinMultiplatform() | `build-logic/convention/.../KotlinAndroid.kt` | Shared Kotlin/Android configuration helper |
+| settings.gradle.kts | Root `settings.gradle.kts` | Module registration and plugin management |
+
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+- **FR-001**: All 27 KMP library modules (`core/*` except `core:api` and `core:barcode`, plus `feature/*` except `feature:widget`) MUST have their `android {}` blocks inside `kotlin {}` migrated to `kotlin.androidLibrary {}` top-level DSL blocks.
+- **FR-002**: Entry-point modules (`app/`, `desktop/`) MUST remain as standalone application modules that depend on shared modules but contain no reusable business logic. This is already satisfied and MUST NOT regress.
+- **FR-003**: The `KmpLibraryConventionPlugin` MUST configure Android target properties (namespace, compileSdk, minSdk, resource settings) exclusively through the `com.android.kotlin.multiplatform.library` plugin's API (`KotlinMultiplatformAndroidLibraryTarget`), not through legacy `android {}` extension blocks.
+- **FR-004**: Android-only modules (`core:api`, `core:barcode`, `feature:widget`, `screenshot-tests`) MUST continue using `com.android.library` or `com.android.application` as they are not multiplatform modules.
+- **FR-005**: The `DESKTOP_ONLY` build mode MUST continue functioning β when active, Android plugin application MUST be skipped for KMP modules (existing `isDesktopOnly` guard preserved).
+- **FR-006**: Module dependency direction MUST remain unidirectional: `app/desktop β feature β core β build-logic`. No reverse dependencies.
+- **FR-007**: The `configureKotlinMultiplatform()` helper function MUST be audited and updated to remove any residual legacy configuration that duplicates or conflicts with the new plugin's target configuration.
+- **FR-008**: Convention plugins MUST prevent future modules from using the legacy `android {}` pattern β applying `meshtastic.kmp.library` MUST automatically configure the Android target through the new plugin's DSL with no manual `android {}` block needed in the module's `build.gradle.kts`.
+
+### Non-Functional Requirements
+
+- **NFR-001**: The restructuring MUST NOT increase full-project build time by more than 5%.
+- **NFR-002**: Gradle configuration cache, isolated projects, and parallel execution (`gradle.properties` settings) MUST remain functional after changes.
+- **NFR-003**: All changes MUST be backward-compatible within a single migration PR β no intermediate broken states on the main branch.
+- **NFR-004**: The migration MUST be documentable as a step-by-step checklist that other Meshtastic platform repositories can reference.
+
+## Source-Set Impact
+
+| Source Set | Impact | Justification |
+| -------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------- |
+| `commonMain` | No changes to source files | Business logic and UI remain in commonMain; only build configuration changes |
+| `androidMain` | No changes to source files | Platform-specific code unchanged; plugin configuration changes only |
+| `jvmMain` | No changes to source files | Desktop-specific code unchanged; plugin configuration changes only |
+| Build files | Modified `build.gradle.kts` across 28 files (27 modules + 1 convention plugin) | Plugin IDs and configuration blocks updated to match recommended patterns |
+| Convention plugins | Modified convention plugin Kotlin files | Updated to enforce recommended plugin application and configuration patterns |
+
+## Privacy Assessment
+
+- [x] No PII, location data, or cryptographic keys logged or exposed
+- [x] No new network calls that transmit user data
+- [x] Proto submodule (`core/proto`) not modified (read-only upstream)
+
+This feature is purely a build infrastructure change with no runtime behavior changes.
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**: All 27 KMP library modules have zero `android {}` blocks inside `kotlin {}` β all Android configuration uses `kotlin.androidLibrary {}` top-level DSL or is handled by convention plugins.
+- **SC-002**: Full project builds succeed on both Android (`assembleDebug`) and Desktop (`:desktop:packageUberJarForCurrentOS`) with zero new errors after migration.
+- **SC-003**: All existing tests pass (`allTests`) with zero regressions after migration.
+- **SC-004**: The `DESKTOP_ONLY` build mode continues to function correctly, building without Android SDK.
+- **SC-005**: Build time for a clean `assembleDebug` does not increase by more than 5% compared to the pre-migration baseline.
+- **SC-006**: A new contributor can add a new KMP module by applying one convention plugin (`meshtastic.kmp.library`) and the module is correctly configured for both Android and Desktop targets with no manual `android {}` block needed.
+- **SC-007**: The `configureKotlinMultiplatform()` helper contains zero references to legacy `android {}` extension configuration β all Android target configuration goes through the new plugin's API.
+
+## Assumptions
+
+- All business logic and UI composables reside in `commonMain` source set (per Constitution Β§I, Β§III).
+- The project already runs AGP 9.2.1 and applies `com.android.kotlin.multiplatform.library` in `KmpLibraryConventionPlugin` β the plugin adoption is complete; only the DSL migration from `android {}` to `kotlin.androidLibrary {}` remains.
+- The `configureKotlinMultiplatform()` helper already uses `KotlinMultiplatformAndroidLibraryTarget` for compileSdk/minSdk configuration β any remaining legacy `android {}` blocks are in individual module `build.gradle.kts` files, not solely in convention plugins.
+- No module uses the deprecated `androidTarget {}` call β this was verified by audit (zero matches found).
+- The typical `android {}` block content in KMP modules is limited to `namespace` and `androidResources.enable = false` and occasionally `withHostTest {}` β migration should be mechanical.
+- Android-only modules (`core:api`, `core:barcode`, `feature:widget`) are not candidates for the KMP library plugin migration β they correctly remain on `com.android.library`.
+- The Gradle version catalog (`libs.versions.toml`) already declares the `android-kotlin-multiplatform-library` plugin alias pointing to AGP 9.2.1.
+- The `jvmAndroidMain` shared source set pattern (used by some modules via `meshtastic.kmp.jvm.android`) is compatible with the new plugin and does not conflict with the `kotlin.androidLibrary {}` DSL.
+- The recommended structure's module naming (`shared`, `androidApp`, `desktopApp`) is a default convention, not a requirement β the project's existing `core/`, `feature/`, `app/`, `desktop/` naming is equally valid per the JetBrains documentation.
diff --git a/specs/006-kmp-project-structure/tasks.md b/specs/006-kmp-project-structure/tasks.md
new file mode 100644
index 0000000000..b9cbdc798b
--- /dev/null
+++ b/specs/006-kmp-project-structure/tasks.md
@@ -0,0 +1,253 @@
+# Tasks: KMP Recommended Project Structure Alignment
+
+**Input**: Design documents from `/specs/006-kmp-project-structure/`
+**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, quickstart.md
+
+**Tests**: No new automated tests are required by this specification. Validation is via existing build and test commands.
+
+**Verification**: Every phase includes constitution-required validation tasks for formatting, static analysis, and the relevant compile/test commands.
+
+**Organization**: Tasks are grouped by user story to enable independent implementation and testing. US4 (Legacy DSL Block Migration) is the primary implementation work, subdivided by migration tier.
+
+## Format: `[ID] [P?] [Story] Description`
+
+- **[P]**: Can run in parallel (different files, no dependencies)
+- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3, US4)
+- Include exact file paths in descriptions
+
+## Phase 1: Setup (Baseline & Prerequisites)
+
+**Purpose**: Establish a clean build baseline and verify the starting state before any changes
+
+- [ ] T001 Verify clean build baseline by running `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` and record clean `assembleDebug` wall-clock time (3 consecutive runs after `./gradlew clean`, median value) to `specs/006-kmp-project-structure/baseline-timing.txt` for NFR-001 comparison
+- [ ] T002 Verify `DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS` passes before migration
+- [x] T003 Run `grep -rn "android {" core/*/build.gradle.kts feature/*/build.gradle.kts` to document all 27 legacy `android {}` blocks as the pre-migration inventory
+
+---
+
+## Phase 2: Convention Plugin Hardening (US2 β Blocking Prerequisite)
+
+**Purpose**: Harden convention plugin defaults so modules can rely on convention for `androidResources.enable = false`. This MUST complete before any module migration.
+
+**Goal**: Convention plugins reflect recommended patterns β `configureKotlinMultiplatform()` absorbs the `androidResources.enable = false` default so 23 of 27 modules no longer need to set it explicitly.
+
+**β οΈ CRITICAL**: No module migration (Phase 3β5) can begin until this phase is complete.
+
+**Independent Test**: `./gradlew assembleDebug` passes β adding the default is backward-compatible since all modules currently override it.
+
+- [x] T004 [US2] Add `androidResources.enable = false` default to `configureKotlinMultiplatform()` inside the `pluginManager.withPlugin` block in `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt` (add after `minSdk` assignment, before namespace auto-derivation)
+- [x] T005 [US2] Verify convention plugin change builds successfully: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests`
+
+**Checkpoint**: Convention plugin hardened β module migration can now begin
+
+---
+
+## Phase 3: Tier 1 Module Migration β Simple Modules (US4, Priority: P1) π― MVP
+
+**Goal**: Migrate 6 modules where convention handles everything β remove `android {}` block entirely, remove redundant `jvm()` declarations, remove explicit `namespace` (auto-derived), remove `androidResources.enable = false` (convention default).
+
+**Independent Test**: `./gradlew assembleDebug allTests` passes after all 6 modules migrated. Verify with `grep -rn "android {" core/di/build.gradle.kts core/nfc/build.gradle.kts core/ui/build.gradle.kts core/navigation/build.gradle.kts feature/settings/build.gradle.kts feature/messaging/build.gradle.kts` returning zero matches.
+
+### Implementation for Tier 1
+
+- [x] T006 [P] [US4] Migrate `core/di/build.gradle.kts` β remove `android {}` block (namespace auto-derived, resources disabled by convention), remove redundant `jvm()` if present
+- [x] T007 [P] [US4] Migrate `core/nfc/build.gradle.kts` β remove `android {}` block (namespace auto-derived, resources disabled by convention)
+- [x] T008 [P] [US4] Migrate `core/ui/build.gradle.kts` β remove `android {}` block (namespace auto-derived, resources disabled by convention), remove redundant `jvm()` if present
+- [x] T009 [P] [US4] Migrate `core/navigation/build.gradle.kts` β remove `android {}` block (namespace auto-derived)
+- [x] T010 [P] [US4] Migrate `feature/settings/build.gradle.kts` β remove `android {}` block (namespace auto-derived, resources disabled by convention)
+- [x] T011 [P] [US4] Migrate `feature/messaging/build.gradle.kts` β remove `android {}` block (namespace auto-derived, resources disabled by convention)
+- [x] T012 [US4] Verify Tier 1 batch: `./gradlew assembleDebug allTests` β commit must be independently buildable (NFR-003)
+
+**Checkpoint**: Tier 1 complete β 6 of 27 modules migrated, build passes
+
+---
+
+## Phase 4: Tier 2 Module Migration β Standard Modules with Host Tests (US4, Priority: P1)
+
+**Goal**: Migrate 18 modules that opt into `withHostTest {}`. Replace `android {}` with `androidLibrary { withHostTest {} }`, remove redundant `jvm()`, remove explicit `namespace` (auto-derived except `feature:wifi-provision`), remove `androidResources.enable = false` (convention default).
+
+**Independent Test**: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` passes after all 18 modules migrated.
+
+### Tier 2a: Modules with `withHostTest { isIncludeAndroidResources = true }` (13 modules)
+
+- [x] T013 [P] [US4] Migrate `core/ble/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T014 [P] [US4] Migrate `core/common/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T015 [P] [US4] Migrate `core/data/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T016 [P] [US4] Migrate `core/domain/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T017 [P] [US4] Migrate `core/model/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove `androidResources.enable = false`
+- [x] T018 [P] [US4] Migrate `core/network/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T019 [P] [US4] Migrate `core/service/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T020 [P] [US4] Migrate `core/takserver/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T021 [P] [US4] Migrate `feature/connections/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T022 [P] [US4] Migrate `feature/firmware/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T023 [P] [US4] Migrate `feature/intro/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T024 [P] [US4] Migrate `feature/map/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T025 [P] [US4] Migrate `feature/node/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove explicit namespace, remove `androidResources.enable = false`
+
+### Tier 2b: Modules with empty `withHostTest {}` (4 modules)
+
+- [x] T026 [P] [US4] Migrate `core/datastore/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest {} }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T027 [P] [US4] Migrate `core/prefs/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest {} }`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T028 [P] [US4] Migrate `core/repository/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest {} }`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T029 [P] [US4] Migrate `core/testing/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest {} }`, remove explicit namespace, remove `androidResources.enable = false`
+
+### Tier 2c: Namespace override required (1 module)
+
+- [x] T030 [P] [US4] Migrate `feature/wifi-provision/build.gradle.kts` β replace `android {}` with `androidLibrary { namespace = "org.meshtastic.feature.wifiprovision"; withHostTest {} }`, remove redundant `jvm()`, remove `androidResources.enable = false` (namespace MUST be explicit β auto-derived `feature.wifi.provision` differs from required `feature.wifiprovision`)
+
+### Tier 2 Verification
+
+- [x] T031 [US4] Verify Tier 2 batch: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` and `DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS` β commit must be independently buildable (NFR-003)
+
+**Checkpoint**: Tiers 1 + 2 complete β 24 of 27 modules migrated, build passes
+
+---
+
+## Phase 5: Tier 3 Module Migration β Special Modules (US4, Priority: P1)
+
+**Goal**: Migrate 3 modules with custom configuration (minSdk override, device tests, resource prefix).
+
+**Independent Test**: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` passes. All special configurations preserved.
+
+### Implementation for Tier 3
+
+- [x] T032 [P] [US4] Migrate `core/proto/build.gradle.kts` β replace `android {}` with `androidLibrary { minSdk = 21 }` (ATAK compatibility override), remove `androidResources.enable = false`
+- [x] T033 [P] [US4] Migrate `core/database/build.gradle.kts` β replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true }; withDeviceTest { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } }`, remove explicit namespace, remove `androidResources.enable = false`
+- [x] T034 [P] [US4] Migrate `core/resources/build.gradle.kts` β replace `android {}` with `androidLibrary { androidResources { enable = true; resourcePrefix = "meshtastic_" }; withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()` (resources MUST override convention default to `enable = true`)
+- [x] T035 [US4] Verify Tier 3 batch: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` β commit must be independently buildable (NFR-003)
+
+**Checkpoint**: All 27 KMP modules migrated β zero legacy `android {}` blocks remain
+
+---
+
+## Phase 6: Full Verification & Build Validation (US1, Priority: P1)
+
+**Goal**: Developer builds successfully after restructuring β full verification across all build modes and configurations.
+
+**Independent Test**: All 4 acceptance scenarios from US1 pass.
+
+- [ ] T036 [US1] Run full Android build: `./gradlew assembleDebug` β verify zero errors
+- [ ] T037 [US1] Run full Desktop build: `./gradlew :desktop:packageUberJarForCurrentOS` β verify zero errors
+- [ ] T038 [US1] Run all tests: `./gradlew allTests` β verify zero regressions
+- [ ] T039 [US1] Run DESKTOP_ONLY mode: `DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS` β verify succeeds without Android SDK
+- [ ] T040 [US1] Compare clean `assembleDebug` time against baseline from T001 (3 consecutive runs after `./gradlew clean`, median value, same machine) β verify <5% increase (NFR-001)
+
+**Checkpoint**: US1 complete β all build modes verified
+
+---
+
+## Phase 7: Polish & Cross-Cutting Concerns
+
+**Purpose**: Final validation, cleanup, and constitution compliance
+
+- [x] T041 [P] Verify no legacy `android {}` blocks remain: `grep -rn "android {" core/*/build.gradle.kts feature/*/build.gradle.kts | grep -v "widget\|api\|barcode\|androidLibrary\|androidResources\|androidMain\|androidHostTest\|androidDeviceTest\|androidRuntimeClasspath"` β must return zero matches (SC-001)
+- [x] T042 [P] Verify no redundant `jvm()` calls remain in migrated modules: `grep -n "jvm()" core/*/build.gradle.kts feature/*/build.gradle.kts | grep -v "widget\|api\|barcode"` β must return zero matches
+- [x] T043 [P] Verify `configureKotlinMultiplatform()` in `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt` uses only `KotlinMultiplatformAndroidLibraryTarget` API β no legacy `android {}` extension configuration (SC-007)
+- [x] T044 [P] [US3] Verify module dependency direction is preserved: no `core/` module depends on `feature/` modules (FR-006)
+- [x] T045 [P] Run constitution-required verification: `./gradlew spotlessApply spotlessCheck detekt`
+- [x] T046 [P] Verify Gradle configuration cache, isolated projects, and parallel execution remain functional (NFR-002): `./gradlew assembleDebug --configuration-cache`
+- [ ] T047 Validate quickstart.md in `specs/006-kmp-project-structure/quickstart.md` matches final implementation β update if migration steps differ from what was executed. Ensure guide is generic enough for other Meshtastic platform repos to reference (NFR-004)
+- [x] T048 [US2] Add FR-008 enforcement: add a CI-time grep check in Phase 7 verification or a Gradle `afterEvaluate` assertion in `configureKotlinMultiplatform()` that fails the build if any KMP module contains a legacy `android {}` block inside `kotlin {}` β prevents future regressions (FR-008)
+- [ ] T049 [P] Verify SC-006: create a temporary scratch module applying `meshtastic.kmp.library`, confirm it configures correctly for Android + Desktop with no manual `android {}` block, then remove it
+
+---
+
+## Dependencies & Execution Order
+
+### Phase Dependencies
+
+- **Setup (Phase 1)**: No dependencies β can start immediately
+- **Convention Plugin Hardening (Phase 2)**: Depends on Phase 1 β BLOCKS all module migrations
+- **Tier 1 Migration (Phase 3)**: Depends on Phase 2 completion
+- **Tier 2 Migration (Phase 4)**: Depends on Phase 2 completion (can run in parallel with Phase 3)
+- **Tier 3 Migration (Phase 5)**: Depends on Phase 2 completion (can run in parallel with Phases 3β4)
+- **Full Verification (Phase 6)**: Depends on Phases 3, 4, and 5 completion
+- **Polish (Phase 7)**: Depends on Phase 6 completion
+
+### User Story Dependencies
+
+- **US2 (Convention Plugins)**: Phase 2 β Must complete first; blocking prerequisite
+- **US4 (Legacy DSL Migration)**: Phases 3, 4, 5 β Primary implementation; depends on US2
+- **US1 (Build Verification)**: Phase 6 β Validation of US4 work; depends on all tiers complete
+- **US3 (Module Boundary Clarity)**: Verified in Phase 7 (T044) β no implementation needed, existing structure already satisfies
+
+### Within Each Tier
+
+- All module migrations within a tier marked [P] can run in parallel (different files, no dependencies)
+- Tier verification task must run after all module tasks in that tier complete
+- Tiers 1, 2, and 3 can themselves run in parallel after Phase 2 completes
+
+### Parallel Opportunities
+
+- **Phase 2**: Sequential (single file change + verification)
+- **Phase 3**: T006βT011 all parallel (6 different `build.gradle.kts` files)
+- **Phase 4**: T013βT030 all parallel (18 different `build.gradle.kts` files)
+- **Phase 5**: T032βT034 all parallel (3 different `build.gradle.kts` files)
+- **Phase 7**: T041βT046 all parallel (independent verification commands)
+- **Cross-tier**: Phases 3, 4, 5 can all run in parallel after Phase 2
+
+---
+
+## Parallel Example: Tier 1 Migration
+
+```text
+# All 6 Tier 1 modules can be migrated simultaneously:
+T006: core/di/build.gradle.kts
+T007: core/nfc/build.gradle.kts
+T008: core/ui/build.gradle.kts
+T009: core/navigation/build.gradle.kts
+T010: feature/settings/build.gradle.kts
+T011: feature/messaging/build.gradle.kts
+
+# Then verify the batch:
+T012: ./gradlew assembleDebug allTests
+```
+
+## Parallel Example: Full Tier Parallelism
+
+```text
+# After Phase 2 (convention plugin hardening), all three tiers can start simultaneously:
+Tier 1 (T006βT012): 6 simple modules β remove android {} entirely
+Tier 2 (T013βT031): 18 standard modules β replace with androidLibrary { withHostTest {} }
+Tier 3 (T032βT035): 3 special modules β custom config (minSdk, device tests, resources)
+
+# Maximum parallelism: 27 module files can be edited simultaneously
+```
+
+---
+
+## Implementation Strategy
+
+### MVP First (Tier 1 Only)
+
+1. Complete Phase 1: Setup (baseline)
+2. Complete Phase 2: Convention plugin hardening (T004βT005)
+3. Complete Phase 3: Tier 1 β 6 simple modules (T006βT012)
+4. **STOP and VALIDATE**: Build passes, 6 modules proven
+5. This alone demonstrates the migration pattern works
+
+### Incremental Delivery
+
+1. Setup + Convention Plugin Hardening β Foundation ready
+2. Tier 1 (6 modules) β Build verified β Pattern proven (MVP!)
+3. Tier 2 (18 modules) β Build verified β Bulk migration complete
+4. Tier 3 (3 modules) β Build verified β All 27 modules done
+5. Full verification + polish β Ready for PR
+
+### Key Risk Mitigation
+
+- Each tier is independently verifiable β if Tier 2 breaks, Tier 1 is still valid
+- Convention plugin change (T004) is backward-compatible β existing explicit overrides in modules are no-ops
+- Individual module migrations can be reverted independently (`git checkout -- <module>/build.gradle.kts`)
+- `feature:wifi-provision` is the only namespace edge case β flagged explicitly in T030
+
+---
+
+## Notes
+
+- [P] tasks = different files, no dependencies on each other
+- [US4] is the primary user story β all module migration tasks belong to it
+- No source code files are modified β only `build.gradle.kts` files and one convention plugin `.kt` file
+- The spec lists 28 modules but `core:model` was counted in both research.md Tier 1 and data-model.md Tier 2 β data-model.md (Tier 2 with `withHostTest`) is authoritative, yielding 27 total KMP modules
+- `feature:widget`, `core:api`, `core:barcode` are Android-only and NOT affected by this migration
+- Commit after each tier verification for clean rollback points
Served by rngit 1.5.4 - Generated in 2.87s